style: auth clean up

This commit is contained in:
Nathnael
2026-07-04 08:22:18 +00:00
parent 619ffa5419
commit de87845f22
8 changed files with 203 additions and 162 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 KiB

View File

@@ -1,32 +1,86 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { ArrowUpRight, ChevronDown, Globe } from "lucide-react"; import { Box, Image, Stack, Text, Title } from "@mantine/core";
import { ChevronDown, Globe } from "lucide-react";
const LOGIN_IMAGE = "/assets/login.png"; const EDR_IMAGE = "/assets/edr_image.png";
const EDR_LOGO = "/assets/logo.svg"; const EDR_LOGO = "/assets/logo.svg";
const LeftPanelDecor = () => ( /** Muted deep-green brand wash for the left panel. */
<div const LEFT_PANEL_BG =
className="pointer-events-none absolute inset-0 overflow-hidden" "linear-gradient(158deg, #2E6B55 0%, #21503F 46%, #16352A 100%)";
aria-hidden
/** 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 }}
> >
<svg {/* Top-left: logo, title, description — stacked, left aligned. */}
className="absolute -bottom-24 -left-24 h-[420px] w-[420px] text-white/[0.07]" <Stack
viewBox="0 0 400 400" gap="xl"
fill="none" className="relative z-10 p-8 lg:p-10"
style={{ maxWidth: 520 }}
> >
{[0, 1, 2, 3, 4, 5].map((ring) => ( <Image
<circle src={EDR_LOGO}
key={ring} alt="EDR Freight"
cx="200" h={40}
cy="200" w="auto"
r={60 + ring * 36} fit="contain"
stroke="currentColor" style={{ filter: "brightness(0) invert(1)", alignSelf: "flex-start" }}
strokeWidth="1" />
/>
))} <Stack gap="sm">
</svg> <Title
<div className="absolute right-0 top-0 h-40 w-40 rounded-full bg-white/[0.06] blur-2xl" /> order={1}
</div> 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 EthioDjibouti 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 = () => ( const RightPanelDecor = () => (
@@ -55,51 +109,6 @@ const RightPanelDecor = () => (
</div> </div>
); );
export interface AuthShellProps {
children: ReactNode;
/** Tagline shown in the highlighted card over the left image panel. */
tagline?: string;
taglineBody?: string;
}
const LeftPanel = ({
tagline,
taglineBody,
}: Pick<AuthShellProps, "tagline" | "taglineBody">) => (
<div className="relative hidden shrink-0 flex-col overflow-hidden rounded-2xl shadow-[0_8px_32px_rgba(15,23,42,0.1)] lg:flex lg:h-auto lg:min-h-0 lg:flex-1 lg:basis-1/2 lg:rounded-[28px]">
<img
src={LOGIN_IMAGE}
alt="Ethio Djibouti Railway"
className="absolute inset-0 h-full w-full object-cover object-center"
/>
<div className="absolute inset-0 bg-gradient-to-br from-[#0a2e1a]/92 via-[#0f4a2a]/55 to-[#1a5c34]/45" />
<LeftPanelDecor />
<div className="relative z-10 flex shrink-0 items-center justify-between px-4 pt-4 sm:px-6 sm:pt-6 lg:px-8 lg:pt-8">
<img
src={EDR_LOGO}
alt="EDR Freight"
className="h-7 w-auto brightness-0 invert sm:h-9"
/>
</div>
<div className="relative z-10 mt-auto hidden px-8 pb-8 lg:block">
<div className="max-w-md rounded-2xl border border-white/15 bg-black/30 p-5 backdrop-blur-md">
<div className="mb-2 flex items-center gap-2">
<div className="h-2 w-2 shrink-0 rounded-full bg-primary" />
<span className="text-sm font-semibold text-white">
{tagline ?? "Empower Your Freight Operations"}
</span>
</div>
<p className="text-sm leading-relaxed text-white/85">
{taglineBody ??
"Sign in to manage bookings, track cargo, and run logistics operations on the Ethio Djibouti Railway freight platform."}
</p>
</div>
</div>
</div>
);
const LanguageSelector = () => ( 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"> <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" /> <Globe className="h-4 w-4 text-gray-500" />
@@ -126,7 +135,7 @@ export default function AuthShell({
</div> </div>
<div className="relative z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain"> <div className="relative z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain">
<div className="flex min-h-full justify-center px-4 py-4 sm:px-6 sm:py-6 lg:px-8 lg:py-8"> <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"> <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} {children}
</div> </div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 KiB

View File

@@ -1,40 +1,25 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { ArrowUpRight, ChevronDown, Globe } from "lucide-react"; import { Box, Image, Stack, Text, Title } from "@mantine/core";
import { ChevronDown, Globe } from "lucide-react";
import { Link } from "react-router-dom";
const LOGIN_IMAGE = "/assets/login.png"; const EDR_IMAGE = "/assets/edr_image.png";
const EDR_LOGO = "/assets/logo.svg"; 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 const fieldClass = export const fieldClass =
"h-11 w-full rounded-xl border border-gray-200/90 bg-white px-4 text-sm text-gray-900 shadow-sm placeholder:text-gray-400 outline-none transition-all duration-200 hover:border-gray-300 focus:border-primary focus:bg-white focus:ring-4 focus:ring-primary/10"; "h-11 w-full rounded-xl border border-gray-200/90 bg-white px-4 text-sm text-gray-900 shadow-sm placeholder:text-gray-400 outline-none transition-all duration-200 hover:border-gray-300 focus:border-primary focus:bg-white focus:ring-4 focus:ring-primary/10";
export const primaryButtonClass = export const primaryButtonClass =
"h-11 w-full rounded-full bg-primary text-sm font-semibold text-primary-foreground shadow-[0_8px_20px_-6px_rgba(16,94,52,0.5)] transition-all duration-200 hover:bg-primary/90 hover:shadow-[0_10px_24px_-6px_rgba(16,94,52,0.55)] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60 disabled:shadow-none"; "h-11 w-full rounded-full bg-primary text-sm font-semibold text-primary-foreground shadow-[0_8px_20px_-6px_rgba(16,94,52,0.5)] transition-all duration-200 hover:bg-primary/90 hover:shadow-[0_10px_24px_-6px_rgba(16,94,52,0.55)] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-60 disabled:shadow-none";
const LeftPanelDecor = () => (
<div
className="pointer-events-none absolute inset-0 overflow-hidden"
aria-hidden
>
<svg
className="absolute -bottom-24 -left-24 h-[420px] w-[420px] text-white/[0.07]"
viewBox="0 0 400 400"
fill="none"
>
{[0, 1, 2, 3, 4, 5].map((ring) => (
<circle
key={ring}
cx="200"
cy="200"
r={60 + ring * 36}
stroke="currentColor"
strokeWidth="1"
/>
))}
</svg>
<div className="absolute right-0 top-0 h-40 w-40 rounded-full bg-white/[0.06] blur-2xl" />
</div>
);
const RightPanelDecor = () => ( const RightPanelDecor = () => (
<div <div
className="pointer-events-none absolute inset-0 overflow-hidden" className="pointer-events-none absolute inset-0 overflow-hidden"
@@ -63,7 +48,7 @@ const RightPanelDecor = () => (
export interface AuthShellProps { export interface AuthShellProps {
children: ReactNode; children: ReactNode;
/** Tagline shown in the highlighted card over the left image panel. */ /** Headline shown in the top-left of the green panel. */
tagline?: string; tagline?: string;
taglineBody?: string; taglineBody?: string;
} }
@@ -72,45 +57,63 @@ const LeftPanel = ({
tagline, tagline,
taglineBody, taglineBody,
}: Pick<AuthShellProps, "tagline" | "taglineBody">) => ( }: Pick<AuthShellProps, "tagline" | "taglineBody">) => (
<div className="relative hidden shrink-0 flex-col overflow-hidden rounded-2xl bg-[#011F12] shadow-[0_8px_32px_rgba(15,23,42,0.1)] lg:flex lg:h-auto lg:min-h-0 lg:flex-1 lg:basis-1/2 lg:rounded-[28px]"> <Box
<img 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]"
src={LOGIN_IMAGE} style={{ background: LEFT_PANEL_BG }}
alt="Ethio Djibouti Railway" >
className="absolute inset-0 h-auto w-full object-cover object-center" {/* Top-left: logo, title, description — stacked, left aligned. */}
/> <Stack
<div className="absolute inset-0 bg-gradient-to-br from-[#0a2e1a]/92 via-[#0f4a2a]/15 to-[#1a5c34]/45" /> gap="xl"
<LeftPanelDecor /> className="relative z-10 p-8 lg:p-10"
style={{ maxWidth: 520 }}
<div className="relative z-10 flex shrink-0 items-center justify-between px-4 pt-4 sm:px-6 sm:pt-6 lg:px-8 lg:pt-8"> >
<img <Image
src={EDR_LOGO} src={EDR_LOGO}
alt="EDR Freight" alt="EDR Freight"
className="h-7 w-auto brightness-0 invert sm:h-9" h={40}
w="auto"
fit="contain"
style={{ filter: "brightness(0) invert(1)", alignSelf: "flex-start" }}
/> />
<a
href="#"
className="flex items-center gap-1.5 rounded-full border border-white/70 bg-white/10 px-3 py-1.5 text-xs font-medium text-white backdrop-blur-sm transition-colors hover:bg-white/20 sm:px-4 sm:py-2 sm:text-sm"
>
Support
<ArrowUpRight className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
</a>
</div>
<div className="relative z-10 mt-auto hidden px-8 pb-8 lg:block"> <Stack gap="sm">
<div className="max-w-md rounded-2xl border border-white/15 bg-black/30 p-5 backdrop-blur-md"> <Title
<div className="mb-2 flex items-center gap-2"> order={1}
<div className="h-2 w-2 shrink-0 rounded-full bg-primary" /> c="white"
<span className="text-sm font-semibold text-white"> fz={38}
{tagline ?? "Empower Your Freight Operations"} fw={800}
</span> lh={1.08}
</div> style={{ letterSpacing: "-0.02em" }}
<p className="text-sm leading-relaxed text-white/85"> >
{tagline ?? "Ethiopian Djibouti Railway"}
</Title>
<Text fz="md" lh={1.6} c="rgba(255,255,255,0.82)" maw={440}>
{taglineBody ?? {taglineBody ??
"Sign in to manage shipments, track cargo, and run logistics operations on the Ethio Djibouti Railway freight platform."} "Sign in to book shipments, track cargo, and manage your freight on the EthioDjibouti Railway platform."}
</p> </Text>
</div> </Stack>
</div> </Stack>
</div>
{/* 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,
opacity: 0.9,
pointerEvents: "none",
maskComposite: "intersect",
}}
/>
</Box>
); );
const LanguageSelector = () => ( const LanguageSelector = () => (
@@ -125,24 +128,24 @@ const FormFooter = () => (
<div className="relative z-10 flex shrink-0 flex-col items-center justify-between gap-3 border-t border-gray-100 px-4 py-4 text-xs text-gray-400 sm:flex-row sm:gap-4 sm:px-6 sm:py-4 lg:px-8 lg:pb-6"> <div className="relative z-10 flex shrink-0 flex-col items-center justify-between gap-3 border-t border-gray-100 px-4 py-4 text-xs text-gray-400 sm:flex-row sm:gap-4 sm:px-6 sm:py-4 lg:px-8 lg:pb-6">
<span className="shrink-0">© 2026 EDR Freight</span> <span className="shrink-0">© 2026 EDR Freight</span>
<div className="flex flex-wrap items-center justify-center gap-3 sm:justify-end sm:gap-6"> <div className="flex flex-wrap items-center justify-center gap-3 sm:justify-end sm:gap-6">
<a <Link
href="#" to="#"
className="font-semibold text-gray-700 transition-colors hover:text-primary" className="font-semibold text-gray-700 transition-colors hover:text-primary"
> >
Terms &amp; Conditions Terms &amp; Conditions
</a> </Link>
<a <Link
href="#" to="#"
className="font-semibold text-gray-700 transition-colors hover:text-primary" className="font-semibold text-gray-700 transition-colors hover:text-primary"
> >
Privacy Policy Privacy Policy
</a> </Link>
<a <Link
href="#" to="#"
className="font-semibold text-gray-700 transition-colors hover:text-primary" className="font-semibold text-gray-700 transition-colors hover:text-primary"
> >
Help &amp; Support Help &amp; Support
</a> </Link>
</div> </div>
</div> </div>
); );
@@ -169,8 +172,8 @@ export default function AuthShell({
</div> </div>
<div className="relative z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain"> <div className="relative z-10 min-h-0 flex-1 overflow-y-auto overscroll-contain">
<div className="flex min-h-full justify-center px-4 py-4 sm:px-6 sm:py-6 lg:px-8 lg:py-8"> <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"> <div className="my-auto w-full max-w-xl rounded-3xl px-5 py-6 sm:px-7 sm:py-8 lg:px-9">
{children} {children}
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { type FormEvent, useState } from "react"; import { type FormEvent, useState } from "react";
import { Alert, Button, PasswordInput, Stack, TextInput } from "@mantine/core"; import { Alert, Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import useAuth from "@/hooks/useAuth"; import useAuth from "@/hooks/useAuth";
import AuthShell from "@/components/auth/AuthShell"; import AuthShell from "@/components/auth/AuthShell";
@@ -79,9 +79,12 @@ export default function LoginPage() {
<div> <div>
<div className="mb-1.5 flex items-center justify-between"> <div className="mb-1.5 flex items-center justify-between">
<span className="text-sm font-medium text-gray-800">Password</span> <span className="text-sm font-medium text-gray-800">Password</span>
<a href="#" className="text-xs font-semibold text-primary hover:underline"> <Link
to="#"
className="text-xs font-semibold text-primary hover:underline"
>
Forgot password? Forgot password?
</a> </Link>
</div> </div>
<PasswordInput <PasswordInput
placeholder="Enter your password" placeholder="Enter your password"

View File

@@ -41,7 +41,10 @@ const passwordRequirements = [
{ label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) }, { label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) },
{ label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) }, { label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) },
{ label: "One number", test: (v: string) => /\d/.test(v) }, { label: "One number", test: (v: string) => /\d/.test(v) },
{ label: "One special character", test: (v: string) => /[^A-Za-z0-9]/.test(v) }, {
label: "One special character",
test: (v: string) => /[^A-Za-z0-9]/.test(v),
},
] as const; ] as const;
const userSchema = z const userSchema = z
@@ -52,8 +55,14 @@ const userSchema = z
.min(1, "Phone number is required") .min(1, "Phone number is required")
.refine(isValidPhone, "Enter a valid phone number"), .refine(isValidPhone, "Enter a valid phone number"),
userType: z.string(), userType: z.string(),
firstName: z.object({ en: z.string().min(2, "Name is required"), am: z.string().nullable() }), firstName: z.object({
lastName: z.object({ en: z.string().min(2, "Name is required"), am: z.string().nullable() }), en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
lastName: z.object({
en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
password: z password: z
.string() .string()
.min(8, "Password must be at least 8 characters") .min(8, "Password must be at least 8 characters")
@@ -239,12 +248,11 @@ export default function SignupPage() {
taglineBody="Join EDR Freight to manage shipments, track consignments, and streamline logistics workflows across Ethiopia and Djibouti." taglineBody="Join EDR Freight to manage shipments, track consignments, and streamline logistics workflows across Ethiopia and Djibouti."
> >
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<div className="mb-4 flex justify-center sm:mb-6">
<img src={EDR_LOGO} alt="EDR Freight" className="h-9 w-auto sm:h-11" />
</div>
{stage === "form" ? ( {stage === "form" ? (
<form onSubmit={handleSubmit(requestOtp)} className="flex w-full flex-col"> <form
onSubmit={handleSubmit(requestOtp)}
className="flex w-full flex-col"
>
<div className="mb-4 space-y-1.5 text-center sm:mb-5"> <div className="mb-4 space-y-1.5 text-center sm:mb-5">
<h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl"> <h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
Create account Create account
@@ -254,7 +262,7 @@ export default function SignupPage() {
</p> </p>
</div> </div>
<Stack gap="md"> <Stack gap="sm">
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md"> <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<TextInput <TextInput
label="First name" label="First name"
@@ -336,15 +344,25 @@ export default function SignupPage() {
{passwordRequirements.map((req) => { {passwordRequirements.map((req) => {
const met = req.test(passwordValue); const met = req.test(passwordValue);
return ( return (
<div key={req.label} className="flex items-center gap-2"> <div
key={req.label}
className="flex items-center gap-2"
>
<span <span
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${ className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${met
met ? "bg-primary text-primary-foreground" : "bg-gray-200 text-gray-500" ? "bg-primary text-primary-foreground"
}`} : "bg-gray-200 text-gray-500"
}`}
> >
{met ? <Check className="h-2.5 w-2.5" /> : <X className="h-2.5 w-2.5" />} {met ? (
<Check className="h-2.5 w-2.5" />
) : (
<X className="h-2.5 w-2.5" />
)}
</span> </span>
<span className={`text-xs ${met ? "text-primary" : "text-gray-500"}`}> <span
className={`text-xs ${met ? "text-primary" : "text-gray-500"}`}
>
{req.label} {req.label}
</span> </span>
</div> </div>
@@ -364,7 +382,11 @@ export default function SignupPage() {
/> />
{error ? ( {error ? (
<Alert color="red" variant="light" icon={<AlertCircle size={18} />}> <Alert
color="red"
variant="light"
icon={<AlertCircle size={18} />}
>
{error} {error}
</Alert> </Alert>
) : null} ) : null}
@@ -414,7 +436,11 @@ export default function SignupPage() {
</div> </div>
{otpError ? ( {otpError ? (
<Alert color="red" variant="light" icon={<AlertCircle size={18} />}> <Alert
color="red"
variant="light"
icon={<AlertCircle size={18} />}
>
{otpError} {otpError}
</Alert> </Alert>
) : null} ) : null}