update authentication pages with new layout and functionality

This commit is contained in:
Marshal
2026-06-17 17:49:16 +00:00
parent 58e6e05458
commit 51e0d4897a
4 changed files with 490 additions and 277 deletions

View File

@@ -12,9 +12,11 @@ import {
Text, Text,
TextInput, TextInput,
} from "@mantine/core"; } from "@mantine/core";
import { Badge as MantineBadge } from "@mantine/core";
import { import {
CheckCircle2, CheckCircle2,
CircleDollarSign, CircleDollarSign,
LayoutGrid,
Loader2, Loader2,
RotateCcw, RotateCcw,
Search, Search,
@@ -24,6 +26,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import Breadcrumbs from "@/components/ui/Breadcrumbs"; import Breadcrumbs from "@/components/ui/Breadcrumbs";
import "@/components/overview/overview.css";
import { usePaymentList, usePaymentSummary } from "@/hooks/usePayments"; import { usePaymentList, usePaymentSummary } from "@/hooks/usePayments";
import type { import type {
PaymentMethod, PaymentMethod,
@@ -39,11 +42,16 @@ import {
} from "@edr/ui-common"; } from "@edr/ui-common";
const STATUS_TABS = [ const STATUS_TABS = [
{ key: "all", label: "All", statuses: undefined as string | undefined }, { key: "all", label: "All", statuses: undefined as string | undefined, icon: LayoutGrid },
{ key: "success", label: "Success", statuses: "success" }, { key: "success", label: "Success", statuses: "success", icon: CheckCircle2 },
{ key: "processing", label: "Processing", statuses: "processing,action-required" }, {
{ key: "failed", label: "Failed", statuses: "failed,canceled" }, key: "processing",
{ key: "refunded", label: "Refunded", statuses: "refunded" }, label: "Processing",
statuses: "processing,action-required",
icon: Loader2,
},
{ key: "failed", label: "Failed", statuses: "failed,canceled", icon: XCircle },
{ key: "refunded", label: "Refunded", statuses: "refunded", icon: RotateCcw },
] as const; ] as const;
type StatusTabKey = (typeof STATUS_TABS)[number]["key"]; type StatusTabKey = (typeof STATUS_TABS)[number]["key"];
@@ -166,6 +174,20 @@ export default function PaymentsPage() {
const val = (n?: number) => (summaryLoading ? "—" : (n ?? 0)); const val = (n?: number) => (summaryLoading ? "—" : (n ?? 0));
const tabCounts: Record<StatusTabKey, number | undefined> = {
all:
summary === undefined
? undefined
: (summary.success ?? 0) +
(summary.processing ?? 0) +
(summary.failed ?? 0) +
(summary.refunded ?? 0),
success: summary?.success,
processing: summary?.processing,
failed: summary?.failed,
refunded: summary?.refunded,
};
const columns: ColumnDef<PaymentRow>[] = [ const columns: ColumnDef<PaymentRow>[] = [
{ {
id: "order", id: "order",
@@ -274,13 +296,48 @@ export default function PaymentsPage() {
setStatusTab((value as StatusTabKey) ?? "all"); setStatusTab((value as StatusTabKey) ?? "all");
setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
}} }}
variant="pills"
color="green"
keepMounted={false}
classNames={{ list: "ov-tablist", tab: "ov-tab" }}
> >
<Tabs.List> <Tabs.List>
{STATUS_TABS.map((t) => ( {STATUS_TABS.map((t) => {
<Tabs.Tab key={t.key} value={t.key}> const isActive = statusTab === t.key;
{t.label} const count = tabCounts[t.key];
</Tabs.Tab> const Icon = t.icon;
))} return (
<Tabs.Tab
key={t.key}
value={t.key}
leftSection={<Icon size={17} strokeWidth={1.85} />}
rightSection={
count !== undefined ? (
<MantineBadge
size="sm"
radius="sm"
variant={isActive ? "white" : "light"}
color={isActive ? "green" : "gray"}
styles={
isActive
? {
root: {
background: "rgba(255,255,255,0.9)",
color: "#15805f",
},
}
: undefined
}
>
{count}
</MantineBadge>
) : undefined
}
>
{t.label}
</Tabs.Tab>
);
})}
</Tabs.List> </Tabs.List>
</Tabs> </Tabs>

View File

@@ -0,0 +1,151 @@
import type { ReactNode } from "react";
import { ArrowUpRight, ChevronDown, Globe } from "lucide-react";
const LOGIN_IMAGE = "/assets/login.png";
const EDR_LOGO = "/assets/logo.svg";
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";
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";
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 = () => (
<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>
);
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" />
<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">
<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 shipments, track cargo, and run logistics operations on the Ethio Djibouti Railway freight platform."}
</p>
</div>
</div>
</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>
);
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">
<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">
<a href="#" className="font-semibold text-gray-700 transition-colors hover:text-primary">
Terms &amp; Conditions
</a>
<a href="#" className="font-semibold text-gray-700 transition-colors hover:text-primary">
Privacy Policy
</a>
<a href="#" className="font-semibold text-gray-700 transition-colors hover:text-primary">
Help &amp; Support
</a>
</div>
</div>
);
export default function AuthShell({ children, tagline, taglineBody }: AuthShellProps) {
return (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div
className="flex h-[100dvh] overflow-hidden bg-[#e8eaef] px-4 py-3 antialiased sm:px-6 sm:py-4 md:px-[70px]"
style={{ fontFamily: "'Outfit', var(--font-sans)" }}
>
<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-[#f5f7fa] 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 px-4 py-4 sm:px-6 sm:py-6 lg:px-8 lg:py-8">
<div className="my-auto w-full max-w-xl rounded-3xl border border-gray-100/80 bg-white px-5 py-6 shadow-[0_4px_24px_rgba(15,23,42,0.06)] sm:px-7 sm:py-8 lg:px-9 lg:py-9">
{children}
</div>
</div>
</div>
<FormFooter />
</div>
</div>
</div>
</>
);
}

View File

@@ -1,39 +1,51 @@
import { Alert, Box, Button, Group, PasswordInput, SegmentedControl, Stack, Text, TextInput } from "@mantine/core"; import { type FormEvent, useState } from "react";
import { ArrowRight, Mail, Phone } from "lucide-react"; import { ChevronDown, Eye, EyeOff, Mail, Smartphone } from "lucide-react";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import useAuth from "@/hooks/useAuth"; import useAuth from "@/hooks/useAuth";
import AuthLayout from "@/components/auth/AuthLayout"; import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
import PhoneInput from "@/components/auth/PhoneInput";
const EDR_LOGO = "/assets/logo.svg";
type LoginMethod = "email" | "phone"; type LoginMethod = "email" | "phone";
const loginMethods: Array<{
value: LoginMethod;
label: string;
icon: typeof Mail;
placeholder: string;
}> = [
{ value: "email", label: "Email", icon: Mail, placeholder: "name@company.com" },
{ value: "phone", label: "Phone", icon: Smartphone, placeholder: "09XXXXXXXX" },
];
export default function LoginPage() { export default function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { login } = useAuth(); const { login } = useAuth();
const [method, setMethod] = useState<LoginMethod>("email"); const [method, setMethod] = useState<LoginMethod>("email");
const [identifier, setIdentifier] = useState(""); const [identifier, setIdentifier] = useState("");
const [countryCode, setCountryCode] = useState("+251"); const [countryCode] = useState("+251");
const [phoneNumber, setPhoneNumber] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => { const currentMethod = loginMethods.find((item) => item.value === method)!;
e.preventDefault();
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setError(null); setError(null);
setLoading(true); setLoading(true);
try { try {
const loginId = const loginId =
method === "email" method === "email"
? identifier ? identifier
: `${countryCode}${phoneNumber.startsWith("0") ? phoneNumber.slice(1) : phoneNumber}`; : `${countryCode}${identifier.startsWith("0") ? identifier.slice(1) : identifier}`;
const result = await login({ email: loginId, password }); const result = await login({ email: loginId, password });
if (result.success) { if (result.success) {
const from = (location.state as { from?: { pathname: string } } | null) const from = (location.state as { from?: { pathname: string } } | null)?.from
?.from?.pathname; ?.pathname;
navigate(from ?? "/portal", { replace: true }); navigate(from ?? "/portal", { replace: true });
} else { } else {
setError(result.error.message); setError(result.error.message);
@@ -46,154 +58,105 @@ export default function LoginPage() {
}; };
return ( return (
<AuthLayout <AuthShell>
left={{ <form className="flex w-full flex-col" onSubmit={handleSubmit}>
badge: "Welcome Back", <div className="mb-4 flex justify-center sm:mb-6">
title: "Sign in to your freight operations account", <img src={EDR_LOGO} alt="EDR Freight" className="h-9 w-auto sm:h-11" />
description: </div>
"Access your dashboard to manage shipments, monitor railway operations, track consignments, and streamline logistics workflows.",
features: [ <div className="mb-4 space-y-1.5 text-center sm:mb-5">
"Real-time shipment tracking", <h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
"Secure logistics management",
"Enterprise-grade operations",
"Multi-corridor freight monitoring",
],
stats: { label: "Active Corridors", value: "24+", footer: "Operational", progress: "w-[95%]" },
}}
>
<Stack gap="xs" mb="lg">
<Box
w={48}
h={48}
bg="edr-soft"
className="flex items-center justify-center rounded-2xl"
>
<Mail size={22} color="var(--mantine-color-edr-green-6)" />
</Box>
<Box>
<Text fz={24} fw={800} c="edr-text" className="tracking-tight">
Welcome back Welcome back
</Text> </h1>
<Text fz={15} c="edr-muted" mt={4}> <p className="text-sm leading-relaxed text-gray-500">
Enter your credentials to access your portal Enter your credentials to access your freight portal.
</Text> </p>
</Box> </div>
</Stack>
<form onSubmit={handleSubmit}> <div className="flex w-full flex-col gap-4">
<Stack gap="md"> <div className="space-y-1.5">
<SegmentedControl <label className="text-sm font-medium text-gray-800">Sign in method</label>
value={method} <div className="relative">
onChange={(v) => setMethod(v as LoginMethod)} <select
fullWidth value={method}
radius="md" onChange={(event) => setMethod(event.target.value as LoginMethod)}
data={[ disabled={loading}
{ className={`${fieldClass} appearance-none pr-10`}
label: (
<Group gap={6} justify="center" wrap="nowrap">
<Mail size={15} />
<Text size="sm">Email</Text>
</Group>
),
value: "email",
},
{
label: (
<Group gap={6} justify="center" wrap="nowrap">
<Phone size={15} />
<Text size="sm">Phone</Text>
</Group>
),
value: "phone",
},
]}
/>
{method === "email" ? (
<TextInput
label="Email Address"
placeholder="name@company.com"
type="email"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
required
disabled={loading}
/>
) : (
<PhoneInput
disabled={loading}
countryCode={{
value: countryCode,
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
setCountryCode(e.target.value),
}}
phone={{
value: phoneNumber,
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
setPhoneNumber(e.target.value),
}}
/>
)}
<Box>
<Group justify="space-between" mb={6}>
<Text size="sm" fw={500} c="edr-text">
Password
</Text>
<Button
variant="transparent"
size="xs"
c="edr-green.6"
p={0}
h="auto"
fz={12}
> >
Forgot password? {loginMethods.map((item) => (
</Button> <option key={item.value} value={item.value}>
</Group> {item.label}
<PasswordInput </option>
placeholder="••••••••" ))}
value={password} </select>
onChange={(e) => setPassword(e.target.value)} <ChevronDown className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
required </div>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-800">
{currentMethod.label} <span className="text-red-500">*</span>
</label>
<input
value={identifier}
onChange={(event) => setIdentifier(event.target.value)}
placeholder={currentMethod.placeholder}
disabled={loading} disabled={loading}
className={fieldClass}
/> />
</Box> </div>
{error && ( <div className="space-y-1.5">
<Alert color="red" variant="light" radius="md"> <div className="flex items-center justify-between">
<label className="text-sm font-medium text-gray-800">
Password <span className="text-red-500">*</span>
</label>
<a href="#" className="text-xs font-semibold text-primary hover:underline">
Forgot password?
</a>
</div>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Enter your password"
disabled={loading}
className={`${fieldClass} pr-11`}
/>
<button
type="button"
onClick={() => setShowPassword((current) => !current)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 transition-colors hover:text-gray-600"
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
{error ? (
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700">
{error} {error}
</Alert> </div>
)} ) : null}
<Button <button type="submit" disabled={loading} className={primaryButtonClass}>
type="submit" {loading ? "Signing in..." : "Sign In"}
disabled={loading} </button>
loading={loading}
size="lg"
color="edr-green"
fullWidth
rightSection={!loading ? <ArrowRight size={18} /> : undefined}
>
Sign In
</Button>
<Text size="sm" c="edr-muted" ta="center"> <p className="text-center text-sm text-gray-500">
Don't have an account?{" "} Don&apos;t have an account?{" "}
<Button <button
variant="transparent" type="button"
p={0}
h="auto"
c="edr-green.6"
fw={600}
fz="sm"
onClick={() => navigate("/signup")} onClick={() => navigate("/signup")}
className="font-semibold text-primary hover:underline"
> >
Create an account Create an account
</Button> </button>
</Text> </p>
</Stack> </div>
</form> </form>
</AuthLayout> </AuthShell>
); );
} }

View File

@@ -1,7 +1,6 @@
import { Alert, Box, Button, Group, PasswordInput, SimpleGrid, Stack, Text, TextInput, ThemeIcon } from "@mantine/core";
import { zodResolver } from "@hookform/resolvers/zod";
import { Check, ArrowRight, UserPlus, X } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRight, Check, Eye, EyeOff, X } from "lucide-react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { z } from "zod"; import { z } from "zod";
@@ -9,8 +8,9 @@ import { z } from "zod";
import { userType } from "@/enums/userType"; import { userType } from "@/enums/userType";
import useAuth from "@/hooks/useAuth"; import useAuth from "@/hooks/useAuth";
import type { SignupPayload } from "@/types/auth"; import type { SignupPayload } from "@/types/auth";
import AuthLayout from "@/components/auth/AuthLayout"; import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
import PhoneInput from "@/components/auth/PhoneInput";
const EDR_LOGO = "/assets/logo.svg";
const passwordRequirements = [ const passwordRequirements = [
{ label: "At least 8 characters", test: (v: string) => v.length >= 8 }, { label: "At least 8 characters", test: (v: string) => v.length >= 8 },
@@ -44,11 +44,16 @@ const userSchema = z
type FormData = z.infer<typeof userSchema>; type FormData = z.infer<typeof userSchema>;
const errorText = (msg?: string) =>
msg ? <p className="mt-1 text-xs text-red-600">{msg}</p> : null;
export default function SignupPage() { export default function SignupPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { signup } = useAuth(); const { signup } = useAuth();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const { const {
register, register,
@@ -102,145 +107,182 @@ export default function SignupPage() {
const passwordValue = watch("password") ?? ""; const passwordValue = watch("password") ?? "";
return ( return (
<AuthLayout <AuthShell
left={{ tagline="Smart Freight Operations"
badge: "Smart Freight Operations", taglineBody="Join EDR Freight to manage shipments, track consignments, and streamline logistics workflows across Ethiopia and Djibouti."
title: "Create your freight operations account",
description:
"Join EDR Freight to manage shipments, monitor railway operations, track consignments, and streamline logistics workflows across Ethiopia and Djibouti.",
features: [
"Real-time shipment tracking",
"Secure logistics management",
"Enterprise-grade operations",
"Multi-corridor freight monitoring",
],
stats: { label: "Active Corridors", value: "24+", footer: "Operational", progress: "w-[95%]" },
}}
> >
<Stack gap="xs" mb="lg"> <form className="flex w-full flex-col" onSubmit={handleSubmit(onSubmit)}>
<Box w={48} h={48} bg="edr-soft" className="flex items-center justify-center rounded-2xl"> <div className="mb-4 flex justify-center sm:mb-6">
<UserPlus size={22} color="var(--mantine-color-edr-green-6)" /> <img src={EDR_LOGO} alt="EDR Freight" className="h-9 w-auto sm:h-11" />
</Box> </div>
<Box>
<Text fz={24} fw={800} c="edr-text" className="tracking-tight"> <div className="mb-4 space-y-1.5 text-center sm:mb-5">
Create Account <h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl">
</Text> Create account
<Text fz={15} c="edr-muted" mt={4}> </h1>
<p className="text-sm leading-relaxed text-gray-500">
Register to access EDR Freight services. Register to access EDR Freight services.
</Text> </p>
</Box> </div>
</Stack>
{error && ( <div className="flex w-full flex-col gap-4">
<Alert color="red" variant="light" radius="md" mb="md"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{error} <div className="space-y-1.5">
</Alert> <label className="text-sm font-medium text-gray-800">
)} First name <span className="text-red-500">*</span>
</label>
<input
placeholder="John"
disabled={loading}
className={fieldClass}
{...register("firstName.en")}
/>
{errorText(errors.firstName?.en?.message)}
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-800">
Last name <span className="text-red-500">*</span>
</label>
<input
placeholder="Doe"
disabled={loading}
className={fieldClass}
{...register("lastName.en")}
/>
{errorText(errors.lastName?.en?.message)}
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)}> <div className="space-y-1.5">
<Stack gap="md"> <label className="text-sm font-medium text-gray-800">
<SimpleGrid cols={2} spacing="md"> Email <span className="text-red-500">*</span>
<TextInput </label>
label="First Name" <input
placeholder="John" type="email"
placeholder="john@example.com"
disabled={loading} disabled={loading}
error={errors.firstName?.en?.message} className={fieldClass}
{...register("firstName.en")} {...register("email")}
/> />
<TextInput {errorText(errors.email?.message)}
label="Last Name" </div>
placeholder="Doe"
disabled={loading}
error={errors.lastName?.en?.message}
{...register("lastName.en")}
/>
</SimpleGrid>
<TextInput <div className="space-y-1.5">
label="Email Address" <label className="text-sm font-medium text-gray-800">
placeholder="john@example.com" Phone <span className="text-red-500">*</span>
type="email" </label>
disabled={loading} <div className="flex items-start gap-2">
error={errors.email?.message} <input
{...register("email")} disabled={loading}
/> className={`${fieldClass} w-20 text-center`}
{...register("countryCode")}
/>
<input
placeholder="912345678"
disabled={loading}
className={`${fieldClass} flex-1`}
{...register("phone")}
/>
</div>
{errorText(errors.countryCode?.message ?? errors.phone?.message)}
</div>
<PhoneInput <div className="space-y-1.5">
disabled={loading} <label className="text-sm font-medium text-gray-800">
countryCode={{ ...register("countryCode") }} Password <span className="text-red-500">*</span>
phone={{ ...register("phone") }} </label>
countryCodeError={errors.countryCode} <div className="relative">
phoneError={errors.phone} <input
/> type={showPassword ? "text" : "password"}
placeholder="Create a strong password"
<Box> disabled={loading}
<PasswordInput className={`${fieldClass} pr-11`}
label="Password" {...register("password")}
placeholder="Create a strong password" />
disabled={loading} <button
error={errors.password?.message} type="button"
{...register("password")} onClick={() => setShowPassword((current) => !current)}
/> className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 transition-colors hover:text-gray-600"
{passwordValue.length > 0 && ( aria-label={showPassword ? "Hide password" : "Show password"}
<Stack gap={4} mt={8}> >
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
{errorText(errors.password?.message)}
{passwordValue.length > 0 ? (
<div className="mt-2 space-y-1">
{passwordRequirements.map((req) => { {passwordRequirements.map((req) => {
const met = req.test(passwordValue); const met = req.test(passwordValue);
return ( return (
<Group key={req.label} gap={6} align="center" wrap="nowrap"> <div key={req.label} className="flex items-center gap-2">
<ThemeIcon <span
size={16} className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${
radius="xl" met ? "bg-primary text-primary-foreground" : "bg-gray-200 text-gray-500"
variant={met ? "filled" : "light"} }`}
color={met ? "edr-green" : "gray"}
> >
{met ? <Check size={10} /> : <X size={10} />} {met ? <Check className="h-2.5 w-2.5" /> : <X className="h-2.5 w-2.5" />}
</ThemeIcon> </span>
<Text size="xs" c={met ? "edr-green.7" : "edr-muted"}> <span className={`text-xs ${met ? "text-primary" : "text-gray-500"}`}>
{req.label} {req.label}
</Text> </span>
</Group> </div>
); );
})} })}
</Stack> </div>
)} ) : null}
</Box> </div>
<PasswordInput <div className="space-y-1.5">
label="Confirm Password" <label className="text-sm font-medium text-gray-800">
placeholder="Re-enter your password" Confirm password <span className="text-red-500">*</span>
disabled={loading} </label>
error={errors.confirmPassword?.message} <div className="relative">
{...register("confirmPassword")} <input
/> type={showConfirm ? "text" : "password"}
placeholder="Re-enter your password"
disabled={loading}
className={`${fieldClass} pr-11`}
{...register("confirmPassword")}
/>
<button
type="button"
onClick={() => setShowConfirm((current) => !current)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 transition-colors hover:text-gray-600"
aria-label={showConfirm ? "Hide password" : "Show password"}
>
{showConfirm ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
{errorText(errors.confirmPassword?.message)}
</div>
<Button {error ? (
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700">
{error}
</div>
) : null}
<button
type="submit" type="submit"
disabled={loading} disabled={loading}
loading={loading} className={`${primaryButtonClass} flex items-center justify-center gap-2`}
size="lg"
color="edr-green"
fullWidth
rightSection={!loading ? <ArrowRight size={18} /> : undefined}
> >
Create Account {loading ? "Creating account..." : "Create Account"}
</Button> {!loading ? <ArrowRight className="h-4 w-4" /> : null}
</button>
<Text size="sm" c="edr-muted" ta="center"> <p className="text-center text-sm text-gray-500">
Already have an account?{" "} Already have an account?{" "}
<Button <button
variant="transparent" type="button"
p={0}
h="auto"
c="edr-green.6"
fw={600}
fz="sm"
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
className="font-semibold text-primary hover:underline"
> >
Sign In Sign In
</Button> </button>
</Text> </p>
</Stack> </div>
</form> </form>
</AuthLayout> </AuthShell>
); );
} }