mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Box, Image, Stack, Text, Title } from "@mantine/core";
|
||||
import { ChevronDown, Globe } from "lucide-react";
|
||||
|
||||
const EDR_IMAGE = "/assets/edr_image.png";
|
||||
const EDR_LOGO = "/assets/logo.svg";
|
||||
|
||||
/** Muted deep-green brand wash for the left panel. */
|
||||
const LEFT_PANEL_BG =
|
||||
"linear-gradient(158deg, #2E6B55 0%, #21503F 46%, #16352A 100%)";
|
||||
|
||||
/** Radial opacity mask: image fully opaque at its center, fading to nothing at the edges. */
|
||||
const IMAGE_FADE_MASK =
|
||||
"linear-gradient(-90deg, #000 95%, #0009 97%, #0000 100%), linear-gradient(0deg, #000 80%, #0001 100%)";
|
||||
|
||||
export interface AuthShellProps {
|
||||
children: ReactNode;
|
||||
/** Headline shown in the top-left of the green panel. */
|
||||
tagline?: string;
|
||||
taglineBody?: string;
|
||||
}
|
||||
|
||||
const LeftPanel = ({
|
||||
tagline,
|
||||
taglineBody,
|
||||
}: Pick<AuthShellProps, "tagline" | "taglineBody">) => (
|
||||
<Box
|
||||
className="relative hidden shrink-0 overflow-hidden rounded-2xl shadow-[0_8px_32px_rgba(15,23,42,0.12)] lg:flex lg:h-auto lg:min-h-0 lg:flex-1 lg:basis-1/2 lg:rounded-[28px]"
|
||||
style={{ background: LEFT_PANEL_BG }}
|
||||
>
|
||||
{/* Top-left: logo, title, description — stacked, left aligned. */}
|
||||
<Stack
|
||||
gap="xl"
|
||||
className="relative z-10 p-8 lg:p-10"
|
||||
style={{ maxWidth: 520 }}
|
||||
>
|
||||
<Image
|
||||
src={EDR_LOGO}
|
||||
alt="EDR Freight"
|
||||
h={40}
|
||||
w="auto"
|
||||
fit="contain"
|
||||
style={{ filter: "brightness(0) invert(1)", alignSelf: "flex-start" }}
|
||||
/>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Title
|
||||
order={1}
|
||||
c="white"
|
||||
fz={38}
|
||||
fw={800}
|
||||
lh={1.08}
|
||||
style={{ letterSpacing: "-0.02em" }}
|
||||
>
|
||||
{tagline ?? "Ethiopian Djibouti Railway"}
|
||||
</Title>
|
||||
<Text fz="md" lh={1.6} c="rgba(255,255,255,0.82)" maw={440}>
|
||||
{taglineBody ??
|
||||
"Manage bookings, track cargo, and run day-to-day logistics for the Ethio–Djibouti Railway from a single backoffice."}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Bottom-right: brand image with a center-to-edge opacity fade, no color tint. */}
|
||||
<Image
|
||||
src={EDR_IMAGE}
|
||||
alt=""
|
||||
aria-hidden
|
||||
fit="cover"
|
||||
pos="absolute"
|
||||
right={0}
|
||||
bottom={0}
|
||||
w="80%"
|
||||
h="60%"
|
||||
style={{
|
||||
WebkitMaskImage: IMAGE_FADE_MASK,
|
||||
maskImage: IMAGE_FADE_MASK,
|
||||
pointerEvents: "none",
|
||||
opacity: 0.9,
|
||||
maskComposite: "intersect",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const RightPanelDecor = () => (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
aria-hidden
|
||||
>
|
||||
<div className="absolute -right-16 -top-20 h-56 w-56 rounded-full bg-primary/[0.06] blur-3xl" />
|
||||
<div className="absolute -bottom-12 left-1/4 h-40 w-40 rounded-full bg-primary/[0.04] blur-2xl" />
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full text-gray-200/40"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="auth-grid"
|
||||
width="28"
|
||||
height="28"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<circle cx="1" cy="1" r="0.75" fill="currentColor" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#auth-grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LanguageSelector = () => (
|
||||
<div className="flex cursor-pointer items-center gap-1.5 rounded-full border border-gray-200/80 bg-white px-3 py-1.5 text-sm text-gray-600 shadow-sm">
|
||||
<Globe className="h-4 w-4 text-gray-500" />
|
||||
<span>Eng</span>
|
||||
<ChevronDown className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function AuthShell({
|
||||
children,
|
||||
tagline,
|
||||
taglineBody,
|
||||
}: AuthShellProps) {
|
||||
return (
|
||||
<div className="flex h-[100dvh] overflow-hidden bg-[#e8eaef] px-4 py-3 antialiased sm:px-6 sm:py-4 md:px-[70px]">
|
||||
<div className="flex h-full min-h-0 w-full flex-col gap-3 lg:flex-row lg:gap-4">
|
||||
<LeftPanel tagline={tagline} taglineBody={taglineBody} />
|
||||
|
||||
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-2xl bg-white shadow-[0_8px_32px_rgba(15,23,42,0.08)] lg:basis-1/2 lg:rounded-[28px]">
|
||||
<RightPanelDecor />
|
||||
|
||||
<div className="relative z-10 flex shrink-0 justify-end px-4 pt-4 sm:px-6 sm:pt-6 lg:px-8 lg:pt-8">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
||||
<div className="flex min-h-full justify-center">
|
||||
<div className="my-auto w-full max-w-xl rounded-3xl px-5 py-6 sm:px-7 sm:py-8 lg:px-9 lg:py-9">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "react";
|
||||
|
||||
import type { SidebarItem, SidebarSection } from "./types";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export interface FreightSidebarProps {
|
||||
sections: SidebarSection[];
|
||||
@@ -35,15 +36,15 @@ const BRAND_LOGO = "/assets/logo.svg";
|
||||
const navClassNames = (active: boolean) =>
|
||||
active
|
||||
? {
|
||||
root: "rounded-md transition-all duration-150 bg-edr-soft! ring-1 ring-inset ring-edr-primary/40 [&_svg]:size-[16px]",
|
||||
label: "text-edr-primary-dark! font-medium! text-sm!",
|
||||
section: "text-edr-primary-dark!",
|
||||
}
|
||||
root: "rounded-md transition-all py-1.5! duration-150 bg-edr-soft! ring-1 ring-inset ring-edr-primary/40 [&_svg]:size-[16px]",
|
||||
label: "text-edr-primary-dark! font-medium! text-sm!",
|
||||
section: "text-edr-primary-dark!",
|
||||
}
|
||||
: {
|
||||
root: "rounded-md transition-all duration-150 hover:bg-[#EEF2F6]! [&_svg]:size-4",
|
||||
label: "text-edr-text! font-medium! text-sm! hover:text-edr-ink!",
|
||||
section: "text-edr-text!",
|
||||
};
|
||||
root: "rounded-md transition-all py-1.5! duration-150 hover:bg-[#EEF2F6]! [&_svg]:size-4",
|
||||
label: "text-edr-text! font-medium! text-sm! hover:text-edr-ink!",
|
||||
section: "text-edr-text!",
|
||||
};
|
||||
|
||||
const itemKey = (parentKey: string, item: SidebarItem, index: number) =>
|
||||
`${parentKey}/${item.href ?? item.label}/${index}`;
|
||||
@@ -65,7 +66,9 @@ const FreightSidebar = ({
|
||||
const isHrefActive = useCallback(
|
||||
(href: string) => {
|
||||
const normalized = href.toLowerCase();
|
||||
return activePath === normalized || activePath.startsWith(`${normalized}/`);
|
||||
return (
|
||||
activePath === normalized || activePath.startsWith(`${normalized}/`)
|
||||
);
|
||||
},
|
||||
[activePath],
|
||||
);
|
||||
@@ -109,9 +112,7 @@ const FreightSidebar = ({
|
||||
|
||||
if (hasChildren) {
|
||||
const isLink = !!item.href;
|
||||
const active =
|
||||
(isLink ? isHrefActive(item.href!) : false) ||
|
||||
branchActive(item.children!);
|
||||
const active = isLink ? isHrefActive(item.href!) : false;
|
||||
const isOpen = openMap[key] ?? false;
|
||||
|
||||
return (
|
||||
@@ -124,7 +125,7 @@ const FreightSidebar = ({
|
||||
active={active}
|
||||
opened={isOpen}
|
||||
classNames={navClassNames(active)}
|
||||
onClick={ () => toggle(key)}
|
||||
onClick={() => toggle(key)}
|
||||
rightSection={
|
||||
<Box
|
||||
component="span"
|
||||
@@ -140,7 +141,9 @@ const FreightSidebar = ({
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="text-edr-muted transition-transform duration-200"
|
||||
style={{ transform: isOpen ? "rotate(-180deg)" : "rotate(180deg)" }}
|
||||
style={{
|
||||
transform: isOpen ? "rotate(-180deg)" : "rotate(180deg)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
@@ -161,8 +164,9 @@ const FreightSidebar = ({
|
||||
label={item.label}
|
||||
leftSection={item.icon}
|
||||
active={active}
|
||||
component={Link}
|
||||
classNames={navClassNames(active)}
|
||||
onClick={() => onNavigate?.(item.href!)}
|
||||
to={item.href!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -178,7 +182,7 @@ const FreightSidebar = ({
|
||||
tt="uppercase"
|
||||
px="sm"
|
||||
mb={6}
|
||||
className={ "text-edr-muted!" }
|
||||
className={"text-edr-muted!"}
|
||||
style={{ fontWeight: 500, fontSize: 10, letterSpacing: "0.05em" }}
|
||||
>
|
||||
{section.title}
|
||||
@@ -232,14 +236,24 @@ const FreightSidebar = ({
|
||||
</Box>
|
||||
</Group>
|
||||
{onClose && (
|
||||
<UnstyledButton onClick={onClose} hiddenFrom="sm" aria-label="Close sidebar">
|
||||
<UnstyledButton
|
||||
onClick={onClose}
|
||||
hiddenFrom="sm"
|
||||
aria-label="Close sidebar"
|
||||
>
|
||||
<X size={18} className="text-edr-muted" strokeWidth={1.8} />
|
||||
</UnstyledButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Nav */}
|
||||
<AppShell.Section grow component={ScrollArea} type="never" px="sm" pb="md">
|
||||
<AppShell.Section
|
||||
grow
|
||||
component={ScrollArea}
|
||||
type="never"
|
||||
px="sm"
|
||||
pb="md"
|
||||
>
|
||||
<Stack gap="lg">{renderedSections}</Stack>
|
||||
</AppShell.Section>
|
||||
</AppShell.Navbar>
|
||||
|
||||
Reference in New Issue
Block a user