mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 13:40:57 +00:00
style(WIP): auth ui clean up.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { ArrowUpRight, ChevronDown, Globe } from "lucide-react";
|
||||
|
||||
const LOGIN_IMAGE = "/assets/login.png";
|
||||
const EDR_LOGO = "/assets/logo.svg";
|
||||
|
||||
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"
|
||||
/>
|
||||
</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 = () => (
|
||||
<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 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 px-5 py-6 sm:px-7 sm:py-8 lg:px-9 lg:py-9">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,162 +1,40 @@
|
||||
import { type FormEvent, useState } from "react";
|
||||
import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
ArrowUpRight,
|
||||
Globe,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Image,
|
||||
PasswordInput,
|
||||
PinInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { AlertCircle, ArrowLeft } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import AuthShell from "@/components/auth/AuthShell";
|
||||
import { extractApiError } from "@/utils/result";
|
||||
|
||||
/** Normalise Ethiopian local phone (09…/07…) to E.164; pass email through unchanged. */
|
||||
const normaliseIdentifier = (raw: string): string => {
|
||||
const v = raw.trim();
|
||||
const digits = v.replace(/\D/g, "");
|
||||
if (digits.length >= 9 && (v.startsWith("0") || v.startsWith("+251"))) {
|
||||
const local = digits.startsWith("251") ? digits.slice(3) : digits.replace(/^0/, "");
|
||||
const local = digits.startsWith("251")
|
||||
? digits.slice(3)
|
||||
: digits.replace(/^0/, "");
|
||||
return `+251${local}`;
|
||||
}
|
||||
return v.toLowerCase();
|
||||
};
|
||||
|
||||
const LOGIN_IMAGE = "/assets/login.png";
|
||||
const EDR_LOGO = "/assets/logo.svg";
|
||||
|
||||
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";
|
||||
|
||||
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="login-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(#login-grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LeftPanel = () => (
|
||||
<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">
|
||||
Empower Your Freight Operations
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-white/85">
|
||||
Sign in to manage bookings, 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 & 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 & Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LoginPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { login, verifyMfa } = useAuth();
|
||||
@@ -165,7 +43,6 @@ const LoginPage = () => {
|
||||
const [otp, setOtp] = useState("");
|
||||
const [needsMfa, setNeedsMfa] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [normalizedIdentifier, setNormalizedIdentifier] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -179,15 +56,14 @@ const LoginPage = () => {
|
||||
setNormalizedIdentifier(normalized);
|
||||
|
||||
const result = await login({ email: normalized, password });
|
||||
console.log(result);
|
||||
if (result.mfaRequired) {
|
||||
setNeedsMfa(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// navigate("/dashboard/overview", { replace: true });
|
||||
} catch {
|
||||
setError("Unable to sign in with those credentials.");
|
||||
} catch (err) {
|
||||
setError(extractApiError(err).message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -201,194 +77,132 @@ const LoginPage = () => {
|
||||
try {
|
||||
await verifyMfa({ email: normalizedIdentifier, otp: otp.trim() });
|
||||
navigate("/dashboard/overview", { replace: true });
|
||||
} catch {
|
||||
setError("Unable to verify the one-time code.");
|
||||
} catch (err) {
|
||||
setError(extractApiError(err).message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loginForm = (
|
||||
<form className="flex w-full flex-col" onSubmit={handleSubmit}>
|
||||
<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>
|
||||
<Box component="form" onSubmit={handleSubmit}>
|
||||
<Center mb={{ base: "md", sm: "lg" }}>
|
||||
<Image src={EDR_LOGO} alt="EDR Freight" h={{ base: 36, sm: 44 }} w="auto" fit="contain" />
|
||||
</Center>
|
||||
|
||||
<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">
|
||||
Get Started
|
||||
</h1>
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
<Stack gap={6} mb={{ base: "md", sm: "lg" }} ta="center">
|
||||
<Title order={1} fz={{ base: "xl", sm: "26px" }} fw={700} lh={1.2}>
|
||||
Welcome back!
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
Log in to access the freight backoffice & explore all logistics
|
||||
resources.
|
||||
</p>
|
||||
</div>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-800">
|
||||
Email or Phone <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={identifier}
|
||||
onChange={(event) => setIdentifier(event.target.value)}
|
||||
placeholder="name@company.com or 09XXXXXXXX"
|
||||
autoComplete="username"
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Email or Phone"
|
||||
placeholder="name@company.com or 09XXXXXXXX"
|
||||
autoComplete="username"
|
||||
required
|
||||
disabled={submitting}
|
||||
value={identifier}
|
||||
onChange={(event) => setIdentifier(event.target.value)}
|
||||
/>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-800">
|
||||
Password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="Enter your password"
|
||||
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>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
disabled={submitting}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700">
|
||||
<Alert color="red" variant="light" icon={<AlertCircle size={18} />}>
|
||||
{error}
|
||||
</div>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className={primaryButtonClass}
|
||||
>
|
||||
{submitting ? "Signing in..." : "Sign In"}
|
||||
</button>
|
||||
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
Need an account?{" "}
|
||||
<a href="#" className="font-semibold text-primary hover:underline">
|
||||
Contact your admin
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
<Button type="submit" color="edr-green" fullWidth loading={submitting}>
|
||||
Sign In
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const mfaForm = (
|
||||
<form className="flex w-full flex-col" onSubmit={handleVerifyMfa}>
|
||||
<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>
|
||||
<Box component="form" onSubmit={handleVerifyMfa}>
|
||||
<Center mb={{ base: "md", sm: "lg" }}>
|
||||
<Image src={EDR_LOGO} alt="EDR Freight" h={{ base: 36, sm: 44 }} w="auto" fit="contain" />
|
||||
</Center>
|
||||
|
||||
<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">
|
||||
<Stack gap={6} mb={{ base: "md", sm: "lg" }} ta="center">
|
||||
<Title order={1} fz={{ base: "xl", sm: "26px" }} fw={700} lh={1.2}>
|
||||
Multi-factor verification
|
||||
</h1>
|
||||
<p className="text-sm leading-relaxed text-gray-500">
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
We sent a verification code to{" "}
|
||||
<span className="font-medium text-gray-700">
|
||||
<Text component="span" fw={500} c="var(--mantine-color-text)">
|
||||
{normalizedIdentifier}
|
||||
</span>
|
||||
</Text>
|
||||
. Enter it below to complete sign in.
|
||||
</p>
|
||||
</div>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-800">
|
||||
Verification code <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
<Stack gap="md">
|
||||
<Stack gap={6} align="center">
|
||||
<Text size="sm" fw={500} c="edr-text">
|
||||
Verification code
|
||||
</Text>
|
||||
<PinInput
|
||||
length={6}
|
||||
type="number"
|
||||
oneTimeCode
|
||||
value={otp}
|
||||
onChange={(event) => setOtp(event.target.value)}
|
||||
placeholder="Enter the code"
|
||||
className={fieldClass}
|
||||
placeholder="0"
|
||||
disabled={submitting}
|
||||
styles={{ input: { textAlign: "center" } }}
|
||||
onChange={setOtp}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700">
|
||||
<Alert color="red" variant="light" icon={<AlertCircle size={18} />}>
|
||||
{error}
|
||||
</div>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<div className="flex w-full gap-3">
|
||||
<button
|
||||
<Group grow>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
leftSection={<ArrowLeft size={14} />}
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setNeedsMfa(false);
|
||||
setOtp("");
|
||||
setError(null);
|
||||
}}
|
||||
className="h-11 min-w-0 flex-1 rounded-full border border-gray-200 bg-white text-sm font-semibold text-gray-700 transition-colors hover:border-gray-300 hover:bg-gray-50"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className={`${primaryButtonClass} min-w-0 flex-1`}
|
||||
color="edr-green"
|
||||
loading={submitting}
|
||||
disabled={otp.trim().length !== 6}
|
||||
>
|
||||
{submitting ? "Verifying..." : "Verify"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
Verify
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
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 />
|
||||
|
||||
<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">
|
||||
{!needsMfa ? loginForm : mfaForm}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <AuthShell>{!needsMfa ? loginForm : mfaForm}</AuthShell>;
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { type FormEvent, useState } from "react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { Alert, Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
|
||||
import AuthShell from "@/components/auth/AuthShell";
|
||||
import { extractApiError } from "@/utils/result";
|
||||
|
||||
const EDR_LOGO = "/assets/edr-logo.png";
|
||||
|
||||
@@ -24,7 +26,6 @@ export default function LoginPage() {
|
||||
const { login } = useAuth();
|
||||
const [identifier, setIdentifier] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -41,8 +42,8 @@ export default function LoginPage() {
|
||||
} else {
|
||||
setError(result.error.message);
|
||||
}
|
||||
} catch {
|
||||
setError("An unexpected error occurred");
|
||||
} catch (err) {
|
||||
setError(extractApiError(err).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -64,60 +65,42 @@ export default function LoginPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium text-gray-800">
|
||||
Email or Phone <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={identifier}
|
||||
onChange={(event) => setIdentifier(event.target.value)}
|
||||
placeholder="name@company.com or 09XXXXXXXX"
|
||||
disabled={loading}
|
||||
autoComplete="username"
|
||||
className={fieldClass}
|
||||
/>
|
||||
</div>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Email or Phone"
|
||||
placeholder="name@company.com or 09XXXXXXXX"
|
||||
autoComplete="username"
|
||||
required
|
||||
disabled={loading}
|
||||
value={identifier}
|
||||
onChange={(event) => setIdentifier(event.target.value)}
|
||||
/>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-gray-800">
|
||||
Password <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-800">Password</span>
|
||||
<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>
|
||||
<PasswordInput
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
disabled={loading}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700">
|
||||
<Alert color="red" variant="light" icon={<AlertCircle size={18} />}>
|
||||
{error}
|
||||
</div>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<button type="submit" disabled={loading} className={primaryButtonClass}>
|
||||
{loading ? "Signing in..." : "Sign In"}
|
||||
</button>
|
||||
<Button type="submit" color="edr-green" fullWidth loading={loading}>
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
Don't have an account?{" "}
|
||||
@@ -129,7 +112,7 @@ export default function LoginPage() {
|
||||
Create an account
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
</AuthShell>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user