mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
style(auth): Apply updated design system guidelines to all authentication forms
This commit is contained in:
@@ -21,7 +21,7 @@ export default function AuthLayout({ children, left }: AuthLayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="grid min-h-screen lg:grid-cols-2">
|
||||
<div className="relative hidden overflow-hidden bg-primary p-12 text-primary-foreground lg:flex lg:flex-col lg:justify-between">
|
||||
<div className="relative hidden overflow-hidden bg-primary p-8 px-12 text-primary-foreground lg:flex lg:flex-col lg:justify-between">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.14),transparent_35%)]" />
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -29,16 +29,22 @@ export default function AuthLayout({ children, left }: AuthLayoutProps) {
|
||||
<Train className="size-7" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black tracking-tight">EDR Freight</h1>
|
||||
<p className="mt-1 text-sm opacity-80">Railway Logistics Platform</p>
|
||||
<h1 className="text-xl font-bold">EDR Freight</h1>
|
||||
<p className="mt-1 text-sm opacity-80">
|
||||
Railway Logistics Platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-20 max-w-lg">
|
||||
<div className="mt-16 max-w-lg">
|
||||
<div className="inline-flex rounded-full bg-white/10 px-4 py-2 text-sm font-semibold backdrop-blur">
|
||||
{left.badge}
|
||||
</div>
|
||||
<h2 className="mt-6 text-5xl font-black leading-tight tracking-tight">{left.title}</h2>
|
||||
<p className="mt-6 text-lg leading-8 opacity-85">{left.description}</p>
|
||||
<h2 className="mt-6 text-4xl font-bold leading-tight tracking-tight">
|
||||
{left.title}
|
||||
</h2>
|
||||
<p className="mt-6 text-lg leading-8 opacity-85">
|
||||
{left.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-14 grid gap-5">
|
||||
{left.features.map((item) => (
|
||||
@@ -51,33 +57,21 @@ export default function AuthLayout({ children, left }: AuthLayoutProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 rounded-[32px] border border-white/10 bg-white/10 p-6 backdrop-blur-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm opacity-80">{left.stats.label}</p>
|
||||
<h3 className="mt-2 text-4xl font-black">{left.stats.value}</h3>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-white/10 px-4 py-2 text-sm font-semibold">{left.stats.footer}</div>
|
||||
</div>
|
||||
<div className="mt-6 h-3 overflow-hidden rounded-full bg-white/10">
|
||||
<div className={`h-full rounded-full bg-white ${left.stats.progress}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-xl">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 flex items-center gap-3 lg:hidden">
|
||||
<div className="flex size-12 items-center justify-center rounded-2xl bg-primary text-primary-foreground">
|
||||
<Train className="size-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">EDR Freight</h1>
|
||||
<p className="text-sm text-muted-foreground">Railway Logistics Platform</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Railway Logistics Platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[36px] border border-border bg-card p-8 shadow-2xl md:p-10">
|
||||
{children}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,14 @@ import { useNavigate } from "react-router-dom";
|
||||
import { ArrowRight, Mail, Phone, Loader2 } from "lucide-react";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import AuthLayout from "@/components/auth/AuthLayout";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
type LoginMethod = "email" | "phone";
|
||||
|
||||
@@ -46,116 +54,117 @@ export default function LoginPage() {
|
||||
"Enterprise-grade operations",
|
||||
"Multi-corridor freight monitoring",
|
||||
],
|
||||
stats: { label: "Active Corridors", value: "24+", footer: "Operational", progress: "w-[95%]" },
|
||||
stats: {
|
||||
label: "Active Corridors",
|
||||
value: "24+",
|
||||
footer: "Operational",
|
||||
progress: "w-[95%]",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<div className="mb-4 flex size-16 items-center justify-center rounded-3xl bg-primary/10 text-primary">
|
||||
<Mail className="size-8" />
|
||||
<div className="mb-6">
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||
<Mail className="size-6" />
|
||||
</div>
|
||||
<h2 className="text-4xl font-black tracking-tight">Welcome back</h2>
|
||||
<p className="mt-3 text-lg text-muted-foreground">
|
||||
<h2 className="text-2xl font-black tracking-tight">Welcome back</h2>
|
||||
<p className="mt-2 text-base text-muted-foreground">
|
||||
Enter your credentials to access your portal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div className="flex gap-2 rounded-lg bg-muted p-1">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant={"ghost"}
|
||||
size="sm"
|
||||
onClick={() => setMethod("email")}
|
||||
className={`flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-all ${
|
||||
method === "email"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
className={`flex-1 hover:bg-background/40! ${method === "email" ? "bg-background shadow border" : ""}`}
|
||||
>
|
||||
<Mail className="size-4" />
|
||||
<Mail data-icon="inline-start" />
|
||||
Email
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"ghost"}
|
||||
size="sm"
|
||||
onClick={() => setMethod("phone")}
|
||||
className={`flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-all ${
|
||||
method === "phone"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
className={`flex-1 hover:bg-background/40! ${method === "phone" ? "bg-background shadow border" : ""}`}
|
||||
>
|
||||
<Phone className="size-4" />
|
||||
<Phone data-icon="inline-start" />
|
||||
Phone
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-semibold">
|
||||
{method === "email" ? "Email Address" : "Phone Number"}
|
||||
</label>
|
||||
<input
|
||||
type={method === "email" ? "email" : "tel"}
|
||||
placeholder={method === "email" ? "name@company.com" : "+251..."}
|
||||
value={identifier}
|
||||
onChange={(e) => setIdentifier(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
className="h-13 w-full rounded-2xl border border-input bg-background px-4 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
</div>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel>
|
||||
{method === "email" ? "Email Address" : "Phone Number"}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
type={method === "email" ? "email" : "tel"}
|
||||
placeholder={method === "email" ? "name@company.com" : "+251..."}
|
||||
value={identifier}
|
||||
onChange={(e) => setIdentifier(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<label className="block text-sm font-semibold">Password</label>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
className="h-13 w-full rounded-2xl border border-input bg-background px-4 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
</div>
|
||||
<Field>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel>Password</FieldLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="xs"
|
||||
className="h-auto p-0"
|
||||
>
|
||||
Forgot password?
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-2.5 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex h-14 w-full items-center justify-center gap-2 rounded-2xl bg-primary text-lg font-semibold text-primary-foreground shadow-lg transition hover:-translate-y-0.5 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<Button type="submit" disabled={loading} size="lg" className="w-full">
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="size-5 animate-spin" />
|
||||
<Loader2 className="animate-spin" data-icon="inline-start" />
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Sign In
|
||||
<ArrowRight className="size-5" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Don't have an account?{" "}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => navigate("/signup")}
|
||||
className="font-semibold text-primary hover:underline"
|
||||
className="h-auto p-0 font-semibold"
|
||||
>
|
||||
Create an account
|
||||
</button>
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
|
||||
@@ -3,9 +3,17 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { ArrowRight, Eye, EyeOff, LockKeyhole } from "lucide-react";
|
||||
import { ArrowRight, Eye, EyeOff, LockKeyhole, Loader2 } from "lucide-react";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import AuthLayout from "@/components/auth/AuthLayout";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
const passwordSchema = z
|
||||
.object({
|
||||
@@ -72,81 +80,91 @@ export default function SetPasswordPage() {
|
||||
stats: { label: "Security Protection", value: "256-bit", footer: "Encrypted", progress: "w-[98%]" },
|
||||
}}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<div className="mb-4 flex size-16 items-center justify-center rounded-3xl bg-primary/10 text-primary">
|
||||
<LockKeyhole className="size-8" />
|
||||
<div className="mb-6">
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||
<LockKeyhole className="size-6" />
|
||||
</div>
|
||||
<h2 className="text-4xl font-black tracking-tight">Set Password</h2>
|
||||
<p className="mt-3 text-lg text-muted-foreground">
|
||||
Create a secure password for your EDR Freight account.
|
||||
<h2 className="text-2xl font-black tracking-tight">Set Password</h2>
|
||||
<p className="mt-2 text-base text-muted-foreground">
|
||||
Create a secure password for your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
<div className="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-2.5 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-semibold">Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Enter password"
|
||||
disabled={loading}
|
||||
{...register("password")}
|
||||
className="h-14 w-full rounded-2xl border border-input bg-background px-4 pr-14 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff className="size-5" /> : <Eye className="size-5" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && <p className="mt-1 text-sm text-red-500">{errors.password.message}</p>}
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<FieldGroup>
|
||||
<Field data-invalid={Boolean(errors.password)}>
|
||||
<FieldLabel>Password</FieldLabel>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Enter password"
|
||||
disabled={loading}
|
||||
aria-invalid={Boolean(errors.password)}
|
||||
className="pr-12"
|
||||
{...register("password")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff /> : <Eye />}
|
||||
</Button>
|
||||
</div>
|
||||
<FieldError errors={[errors.password]} />
|
||||
</Field>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-semibold">Confirm Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
placeholder="Confirm password"
|
||||
disabled={loading}
|
||||
{...register("confirmPassword")}
|
||||
className="h-14 w-full rounded-2xl border border-input bg-background px-4 pr-14 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff className="size-5" /> : <Eye className="size-5" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.confirmPassword && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.confirmPassword.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<Field data-invalid={Boolean(errors.confirmPassword)}>
|
||||
<FieldLabel>Confirm Password</FieldLabel>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
placeholder="Confirm password"
|
||||
disabled={loading}
|
||||
aria-invalid={Boolean(errors.confirmPassword)}
|
||||
className="pr-12"
|
||||
{...register("confirmPassword")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff /> : <Eye />}
|
||||
</Button>
|
||||
</div>
|
||||
<FieldError errors={[errors.confirmPassword]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex h-14 w-full items-center justify-center gap-2 rounded-2xl bg-primary text-lg font-semibold text-primary-foreground shadow-lg transition hover:-translate-y-0.5 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
{loading ? (
|
||||
"Saving..."
|
||||
<>
|
||||
<Loader2 className="animate-spin" data-icon="inline-start" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Save Password
|
||||
<ArrowRight className="size-5" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,19 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { ArrowRight, UserPlus } from "lucide-react";
|
||||
import { ArrowRight, UserPlus, Loader2 } from "lucide-react";
|
||||
import { userType } from "@/enums/userType";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import type { SignupPayload } from "@/types/auth";
|
||||
import AuthLayout from "@/components/auth/AuthLayout";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
const userSchema = z.object({
|
||||
email: z.string().email("Invalid email address"),
|
||||
@@ -86,107 +94,115 @@ export default function SignupPage() {
|
||||
stats: { label: "Active Corridors", value: "24+", footer: "Operational", progress: "w-[95%]" },
|
||||
}}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<div className="mb-4 flex size-16 items-center justify-center rounded-3xl bg-primary/10 text-primary">
|
||||
<UserPlus className="size-8" />
|
||||
<div className="mb-6">
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||
<UserPlus className="size-6" />
|
||||
</div>
|
||||
<h2 className="text-4xl font-black tracking-tight">Create Account</h2>
|
||||
<p className="mt-3 text-lg text-muted-foreground">
|
||||
Register to access EDR Freight services and railway logistics operations.
|
||||
<h2 className="text-2xl font-black tracking-tight">Create Account</h2>
|
||||
<p className="mt-2 text-base text-muted-foreground">
|
||||
Register to access EDR Freight services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
<div className="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-2.5 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-semibold">Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
disabled={loading}
|
||||
{...register("name.en")}
|
||||
className="h-13 w-full rounded-2xl border border-input bg-background px-4 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
{errors.name?.en && <p className="mt-1 text-sm text-red-500">{errors.name.en.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-semibold">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="john_doe"
|
||||
disabled={loading}
|
||||
{...register("username")}
|
||||
className="h-13 w-full rounded-2xl border border-input bg-background px-4 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
{errors.username && <p className="mt-1 text-sm text-red-500">{errors.username.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-semibold">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
disabled={loading}
|
||||
{...register("email")}
|
||||
className="h-13 w-full rounded-2xl border border-input bg-background px-4 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-semibold">Phone Number</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<FieldGroup className="gap-4">
|
||||
<Field data-invalid={Boolean(errors.name?.en)}>
|
||||
<FieldLabel>Full Name</FieldLabel>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
disabled={loading}
|
||||
{...register("countryCode")}
|
||||
className="h-13 w-28 rounded-2xl border border-input bg-background px-4 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
aria-invalid={Boolean(errors.name?.en)}
|
||||
{...register("name.en")}
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="912345678"
|
||||
disabled={loading}
|
||||
{...register("phone")}
|
||||
className="h-13 flex-1 rounded-2xl border border-input bg-background px-4 outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
</div>
|
||||
{(errors.countryCode || errors.phone) && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.countryCode?.message || errors.phone?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<FieldError errors={[errors.name?.en]} />
|
||||
</Field>
|
||||
|
||||
<button
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={Boolean(errors.username)}>
|
||||
<FieldLabel>Username</FieldLabel>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="john_doe"
|
||||
disabled={loading}
|
||||
aria-invalid={Boolean(errors.username)}
|
||||
{...register("username")}
|
||||
/>
|
||||
<FieldError errors={[errors.username]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(errors.email)}>
|
||||
<FieldLabel>Email Address</FieldLabel>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
disabled={loading}
|
||||
aria-invalid={Boolean(errors.email)}
|
||||
{...register("email")}
|
||||
/>
|
||||
<FieldError errors={[errors.email]} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field data-invalid={Boolean(errors.countryCode || errors.phone)}>
|
||||
<FieldLabel>Phone Number</FieldLabel>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
disabled={loading}
|
||||
className="w-20"
|
||||
aria-invalid={Boolean(errors.countryCode)}
|
||||
{...register("countryCode")}
|
||||
/>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="912345678"
|
||||
disabled={loading}
|
||||
className="flex-1"
|
||||
aria-invalid={Boolean(errors.phone)}
|
||||
{...register("phone")}
|
||||
/>
|
||||
</div>
|
||||
<FieldError errors={[errors.countryCode, errors.phone]} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex h-14 w-full items-center justify-center gap-2 rounded-2xl bg-primary text-lg font-semibold text-primary-foreground shadow-lg transition hover:-translate-y-0.5 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
{loading ? (
|
||||
"Creating..."
|
||||
<>
|
||||
<Loader2 className="animate-spin" data-icon="inline-start" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Create Account
|
||||
<ArrowRight className="size-5" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Already have an account?
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => navigate("/login")}
|
||||
className="ml-2 font-semibold text-primary hover:underline"
|
||||
className="h-auto p-0 font-semibold"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
|
||||
@@ -3,10 +3,18 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { ArrowRight, MailCheck, RotateCw } from "lucide-react";
|
||||
import { ArrowRight, MailCheck, RotateCw, Loader2 } from "lucide-react";
|
||||
import { verificationCodeType } from "@/enums/verificationCodeType";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import AuthLayout from "@/components/auth/AuthLayout";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
const otpSchema = z.object({
|
||||
code: z.string().regex(/^\d{6}$/, "OTP must be exactly 6 digits"),
|
||||
@@ -88,92 +96,105 @@ export default function VerificationOtpPage() {
|
||||
stats: { label: "Verification Security", value: "99.9%", footer: "Protected", progress: "w-[99%]" },
|
||||
}}
|
||||
>
|
||||
<div className="mb-8">
|
||||
<div className="mb-4 flex size-16 items-center justify-center rounded-3xl bg-primary/10 text-primary">
|
||||
<MailCheck className="size-8" />
|
||||
<div className="mb-6">
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||
<MailCheck className="size-6" />
|
||||
</div>
|
||||
<h2 className="text-4xl font-black tracking-tight">OTP Verification</h2>
|
||||
<p className="mt-3 text-lg text-muted-foreground">Enter the 6-digit code sent to:</p>
|
||||
<div className="mt-4 rounded-2xl border border-border bg-muted/50 px-4 py-3">
|
||||
<p className="font-semibold">{maskedPhone}</p>
|
||||
<h2 className="text-2xl font-black tracking-tight">OTP Verification</h2>
|
||||
<p className="mt-2 text-base text-muted-foreground">Enter the 6-digit code sent to:</p>
|
||||
<div className="mt-3 rounded-xl border border-border bg-muted/50 px-4 py-2">
|
||||
<p className="text-sm font-semibold">{maskedPhone}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
<div className="mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-2.5 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resentMessage && (
|
||||
<div className="mb-6 rounded-2xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-700">
|
||||
<div className="mb-4 rounded-xl border border-blue-200 bg-blue-50 px-4 py-2.5 text-sm text-blue-700">
|
||||
{resentMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-semibold">Verification Code</label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
maxLength={6}
|
||||
placeholder="123456"
|
||||
disabled={verifying}
|
||||
{...register("code")}
|
||||
className="h-16 w-full rounded-2xl border border-input bg-background px-5 text-center text-3xl font-black tracking-[12px] outline-none transition focus:border-primary focus:ring-4 focus:ring-primary/10 disabled:opacity-60"
|
||||
/>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
{errors.code ? (
|
||||
<p className="text-sm text-red-500">{errors.code.message}</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Enter the OTP sent to your phone</p>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{otpValue.length}/6</span>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<FieldGroup>
|
||||
<Field data-invalid={Boolean(errors.code)}>
|
||||
<FieldLabel>Verification Code</FieldLabel>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
maxLength={6}
|
||||
placeholder="123456"
|
||||
disabled={verifying}
|
||||
aria-invalid={Boolean(errors.code)}
|
||||
className="h-14 text-center text-2xl font-black tracking-[10px]"
|
||||
{...register("code")}
|
||||
/>
|
||||
<div className="mt-1.5 flex items-center justify-between">
|
||||
{errors.code ? (
|
||||
<FieldError errors={[errors.code]} />
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">Enter the OTP sent to your phone</p>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{otpValue.length}/6</span>
|
||||
</div>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={verifying || otpValue.length !== 6}
|
||||
className="flex h-14 w-full items-center justify-center gap-2 rounded-2xl bg-primary text-lg font-semibold text-primary-foreground shadow-lg transition hover:-translate-y-0.5 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
{verifying ? (
|
||||
"Verifying..."
|
||||
<>
|
||||
<Loader2 className="animate-spin" data-icon="inline-start" />
|
||||
Verifying...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Verify Account
|
||||
<ArrowRight className="size-5" />
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleResend}
|
||||
disabled={resending}
|
||||
className="flex h-14 w-full items-center justify-center gap-2 rounded-2xl border border-border bg-background text-base font-semibold transition hover:bg-muted disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="w-full"
|
||||
>
|
||||
{resending ? (
|
||||
"Sending..."
|
||||
<>
|
||||
<Loader2 className="animate-spin" data-icon="inline-start" />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RotateCw className="size-5" />
|
||||
<RotateCw data-icon="inline-start" />
|
||||
Resend Code
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Didn't receive the code?
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={handleResend}
|
||||
className="ml-2 font-semibold text-primary hover:underline"
|
||||
className="h-auto p-0 font-semibold"
|
||||
>
|
||||
Send again
|
||||
</button>
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
|
||||
Reference in New Issue
Block a user