refactor(auth): Transition main app to custom authentication system and local useAuth hook

This commit is contained in:
ghost2023
2026-05-28 11:41:23 +03:00
parent 53e8b40123
commit b30a04edce
6 changed files with 696 additions and 1339 deletions

View File

@@ -19,6 +19,7 @@ import {
UserCircle,
FileUp,
MapPinned,
Loader2,
} from "lucide-react";
import BookingsPage from "./pages/bookings/BookingsPage";
@@ -31,12 +32,7 @@ import TrackingPage from "./pages/tracking/TrackingPage";
import BillingPage from "./pages/billing/BillingPage";
import TrainsPage from "./pages/trains/TrainsPage";
import DashboardPage from "./pages/dashboard/DashboardPage";
import {
IamLoginPage,
LoadingScreen,
useAuth,
useAuthUser,
} from "@tria-plc/iamui-common";
import useAuth from "./hooks/useAuth";
import CustomersPage from "./pages/customers/CustomersPage";
import CustomerDetailPage from "./pages/customers/CustomerDetailPage";
import NewCustomerPage from "./pages/customers/NewCustomerPage";
@@ -48,6 +44,7 @@ import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
import SignupPage from "./pages/accounts/SignupPage";
import VerificationOtpPage from "./pages/accounts/VerificationOtpPage";
import SetPasswordPage from "./pages/accounts/SetPasswordPage";
import LoginPage from "./pages/accounts/LoginPage";
import Station from "./components/stations/Station";
const sidebarItems: SidebarItem[] = [
@@ -72,22 +69,26 @@ const sidebarItems: SidebarItem[] = [
const App = () => {
const navigate = useNavigate();
const location = useLocation();
const { user, loading } = useAuth();
const { logout } = useAuthUser();
const { user, isPending, logout } = useAuth();
if (loading) {
return <LoadingScreen />;
console.log({ user, isPending });
if (isPending) {
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="animate-spin text-primary" />
</div>
);
}
if (user) {
if (!user) {
return (
<Routes>
<Route path="/" element={<EDRFreightLandingPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
<Route path="/otp" element={<VerificationOtpPage />} />
<Route path="/set-password" element={<SetPasswordPage />} />
<Route path="/auth" element={<IamLoginPage />} />
{/* <Route path="*" element={<Navigate to="/auth" replace />} /> */}
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
);
}
@@ -95,21 +96,6 @@ const App = () => {
const displayName = user?.name?.en || user?.username || user?.email || "User";
const userEmail = user?.email;
const handleLogout = () => {
logout();
[
"auth-token",
"refresh-token",
"auth-user",
"current-position-id",
"selected-position-id",
].forEach((name) => {
document.cookie = `${name}=; Max-Age=0; path=/`;
});
localStorage.clear();
window.location.replace("/auth");
};
return (
<DashboardLayout
title="EDR Freight"
@@ -119,7 +105,7 @@ const App = () => {
enableThemeToggle
userName={displayName}
userEmail={userEmail}
onLogout={handleLogout}
onLogout={logout}
>
<Routes>
<Route path="/dashboard" element={<DashboardPage />} />

View File

@@ -0,0 +1,86 @@
import type { ReactNode } from "react";
import { ShieldCheck, Train } from "lucide-react";
export interface AuthLayoutProps {
children: ReactNode;
left: {
badge: string;
title: string;
description: string;
features: string[];
stats: {
label: string;
value: string;
footer: string;
progress: string;
};
};
}
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="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">
<div className="flex size-14 items-center justify-center rounded-3xl bg-white/10 backdrop-blur">
<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>
</div>
</div>
<div className="mt-20 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>
</div>
<div className="mt-14 grid gap-5">
{left.features.map((item) => (
<div key={item} className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-2xl bg-white/10">
<ShieldCheck className="size-5" />
</div>
<span className="font-medium">{item}</span>
</div>
))}
</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="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>
</div>
</div>
<div className="rounded-[36px] border border-border bg-card p-8 shadow-2xl md:p-10">
{children}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,163 @@
import { useState } from "react";
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";
type LoginMethod = "email" | "phone";
export default function LoginPage() {
const navigate = useNavigate();
const { login } = useAuth();
const [method, setMethod] = useState<LoginMethod>("email");
const [identifier, setIdentifier] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const result = await login({ email: identifier, password });
if (result.success) {
navigate("/");
} else {
setError(result.error.message);
}
} catch {
setError("An unexpected error occurred");
} finally {
setLoading(false);
}
};
return (
<AuthLayout
left={{
badge: "Welcome Back",
title: "Sign in to your freight operations account",
description:
"Access your dashboard to manage shipments, monitor railway operations, track consignments, and streamline logistics workflows.",
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%]" },
}}
>
<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>
<h2 className="text-4xl font-black tracking-tight">Welcome back</h2>
<p className="mt-3 text-lg text-muted-foreground">
Enter your credentials to access your portal
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="flex gap-2 rounded-lg bg-muted p-1">
<button
type="button"
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"
}`}
>
<Mail className="size-4" />
Email
</button>
<button
type="button"
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"
}`}
>
<Phone className="size-4" />
Phone
</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>
<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>
{error && (
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 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"
>
{loading ? (
<>
<Loader2 className="size-5 animate-spin" />
Signing in...
</>
) : (
<>
Sign In
<ArrowRight className="size-5" />
</>
)}
</button>
<p className="text-center text-sm text-muted-foreground">
Don't have an account?{" "}
<button
type="button"
onClick={() => navigate("/signup")}
className="font-semibold text-primary hover:underline"
>
Create an account
</button>
</p>
</form>
</AuthLayout>
);
}

View File

@@ -1,418 +1,153 @@
import { setPassword } from "@/services/account";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import {
ArrowRight,
LockKeyhole,
ShieldCheck,
Train,
Eye,
EyeOff,
} from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// -----------------------------------------------------------------------------
// Schema
// -----------------------------------------------------------------------------
import { ArrowRight, Eye, EyeOff, LockKeyhole } from "lucide-react";
import useAuth from "@/hooks/useAuth";
import AuthLayout from "@/components/auth/AuthLayout";
const passwordSchema = z
.object({
password: z
.string()
.min(
8,
"Password must be at least 8 characters"
),
confirmPassword: z
.string()
.min(
8,
"Confirm password is required"
),
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string().min(8, "Confirm password is required"),
})
.refine(
(data) =>
data.password ===
data.confirmPassword,
{
message:
"Passwords do not match",
path: ["confirmPassword"],
}
);
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
type FormData = z.infer<
typeof passwordSchema
>;
// -----------------------------------------------------------------------------
// Component
// -----------------------------------------------------------------------------
type FormData = z.infer<typeof passwordSchema>;
export default function SetPasswordPage() {
const [
showPassword,
setShowPassword,
] = useState(false);
const [
showConfirmPassword,
setShowConfirmPassword,
] = useState(false);
const navigate = useNavigate();
const { setPassword } = useAuth();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<FormData>({
resolver:
zodResolver(passwordSchema),
defaultValues: {
password: "",
confirmPassword: "",
},
resolver: zodResolver(passwordSchema),
defaultValues: { password: "", confirmPassword: "" },
});
const naviagte = useNavigate();
// ---------------------------------------------------------------------------
// Mutation
// ---------------------------------------------------------------------------
const setPasswordMutation =
useMutation({
mutationFn: async (
data: FormData
) => setPassword({
newPassword: data?.password,
confirmPassword: data?.confirmPassword,
userId: localStorage.getItem("userId"),
email: localStorage.getItem("otp-email"),
verificationCode: localStorage.getItem("otp"),
}),
onSuccess: () => {
naviagte("/auth");
reset();
},
});
// ---------------------------------------------------------------------------
// Submit
// ---------------------------------------------------------------------------
const onSubmit = async (
data: FormData
) => {
const onSubmit = async (data: FormData) => {
setError(null);
setLoading(true);
try {
await setPasswordMutation.mutateAsync(
data
);
} catch (err) {
console.error(err);
const result = await setPassword({
newPassword: data.password,
confirmPassword: data.confirmPassword,
});
if (result.success) {
navigate("/auth");
} else {
setError(result.error.message);
}
} catch {
setError("An unexpected error occurred");
} finally {
setLoading(false);
}
};
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
return (
<div className="min-h-screen bg-background text-foreground">
<div className="grid min-h-screen lg:grid-cols-2">
{/* ------------------------------------------------------------------ */}
{/* Left Side */}
{/* ------------------------------------------------------------------ */}
<div className="relative hidden overflow-hidden bg-primary p-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">
{/* Logo */}
<div className="flex items-center gap-3">
<div className="flex size-14 items-center justify-center rounded-3xl bg-white/10 backdrop-blur">
<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>
</div>
</div>
{/* Hero */}
<div className="mt-20 max-w-lg">
<div className="inline-flex rounded-full bg-white/10 px-4 py-2 text-sm font-semibold backdrop-blur">
Account Security
</div>
<h2 className="mt-6 text-5xl font-black leading-tight tracking-tight">
Set your secure
password
</h2>
<p className="mt-6 text-lg leading-8 opacity-85">
Create a strong
password to secure
your EDR Freight
account and protect
railway logistics
operations and shipment
data.
</p>
</div>
{/* Features */}
<div className="mt-14 grid gap-5">
{[
"Enterprise-grade security",
"Protected account access",
"Secure freight operations",
"Advanced authentication system",
].map((item) => (
<div
key={item}
className="flex items-center gap-3"
>
<div className="flex size-10 items-center justify-center rounded-2xl bg-white/10">
<ShieldCheck className="size-5" />
</div>
<span className="font-medium">
{item}
</span>
</div>
))}
</div>
</div>
{/* Stats */}
<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">
Security Protection
</p>
<h3 className="mt-2 text-4xl font-black">
256-bit
</h3>
</div>
<div className="rounded-2xl bg-white/10 px-4 py-2 text-sm font-semibold">
Encrypted
</div>
</div>
<div className="mt-6 h-3 overflow-hidden rounded-full bg-white/10">
<div className="h-full w-[98%] rounded-full bg-white" />
</div>
</div>
</div>
{/* ------------------------------------------------------------------ */}
{/* Right Side */}
{/* ------------------------------------------------------------------ */}
<div className="flex items-center justify-center p-6 md:p-10">
<div className="w-full max-w-xl">
{/* Mobile Logo */}
<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>
</div>
</div>
{/* Card */}
<div className="rounded-[36px] border border-border bg-card p-8 shadow-2xl md:p-10">
{/* Header */}
<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>
<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.
</p>
</div>
{/* Success */}
{setPasswordMutation.isSuccess && (
<div className="mb-6 rounded-2xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
Password updated
successfully.
</div>
)}
{/* Error */}
{setPasswordMutation.isError && (
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
Failed to set
password. Please try
again.
</div>
)}
{/* Form */}
<form
onSubmit={handleSubmit(
onSubmit
)}
className="space-y-6"
>
{/* Password */}
<div>
<label className="mb-2 block text-sm font-semibold">
Password
</label>
<div className="relative">
<input
type={
showPassword
? "text"
: "password"
}
placeholder="Enter password"
disabled={
setPasswordMutation.isPending
}
{...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>
{/* Confirm Password */}
<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={
setPasswordMutation.isPending
}
{...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>
{/* Submit */}
<button
type="submit"
disabled={
setPasswordMutation.isPending
}
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"
>
{setPasswordMutation.isPending ? (
"Saving..."
) : (
<>
Save Password
<ArrowRight className="size-5" />
</>
)}
</button>
</form>
</div>
</div>
<AuthLayout
left={{
badge: "Account Security",
title: "Set your secure password",
description:
"Create a strong password to secure your EDR Freight account and protect railway logistics operations and shipment data.",
features: [
"Enterprise-grade security",
"Protected account access",
"Secure freight operations",
"Advanced authentication system",
],
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>
<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.
</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">
{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>
<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>
<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"
>
{loading ? (
"Saving..."
) : (
<>
Save Password
<ArrowRight className="size-5" />
</>
)}
</button>
</form>
</AuthLayout>
);
}
}

View File

@@ -1,534 +1,194 @@
import { userType } from "@/enums/userType";
import { createOTP, createUser } from "@/services/account";
import { CreateUserPayload } from "@/types/createUser";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import {
ArrowRight,
ShieldCheck,
Train,
UserPlus,
} from "lucide-react";
import { useForm } from "react-hook-form";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// -----------------------------------------------------------------------------
// Schema
// -----------------------------------------------------------------------------
import { ArrowRight, UserPlus } 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";
const userSchema = z.object({
email: z
.string()
.email("Invalid email address"),
username: z
.string()
.min(
3,
"Username must be at least 3 characters"
),
countryCode: z
.string()
.min(
1,
"Country code is required"
),
phone: z
.string()
.min(
9,
"Phone number is too short"
)
.max(
9,
"Phone number is too long"
),
email: z.string().email("Invalid email address"),
username: z.string().min(3, "Username must be at least 3 characters"),
countryCode: z.string().min(1, "Country code is required"),
phone: z.string().min(9, "Phone number is too short").max(9, "Phone number is too long"),
userType: z.string(),
name: z.object({
en: z
.string()
.min(2, "Name is required"),
en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
});
type FormData = z.infer<
typeof userSchema
>;
// -----------------------------------------------------------------------------
// Component
// -----------------------------------------------------------------------------
type FormData = z.infer<typeof userSchema>;
export default function SignupPage() {
const navigate = useNavigate();
const { signup } = useAuth();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<FormData>({
resolver:
zodResolver(userSchema),
resolver: zodResolver(userSchema),
defaultValues: {
email: "",
username: "",
countryCode: "+251",
phone: "",
userType:
userType.individual,
name: {
en: "",
am: "",
},
userType: userType.individual,
name: { en: "", am: "" },
},
});
// ---------------------------------------------------------------------------
// Create User Mutation
// ---------------------------------------------------------------------------
const createUserMutation =
useMutation({
mutationFn: (
user: CreateUserPayload
) => createUser(user),
onSuccess: () => {
reset();
},
});
// ---------------------------------------------------------------------------
// Submit
// ---------------------------------------------------------------------------
const onSubmit = async (
data: FormData
) => {
const onSubmit = async (data: FormData) => {
setError(null);
setLoading(true);
try {
const normalizedPhone =
data.phone.startsWith(
"0"
)
? data.phone.slice(1)
: data.phone;
const fullPhoneNumber = `${data.countryCode
}${normalizedPhone}`;
const payload: CreateUserPayload =
{
const normalizedPhone = data.phone.startsWith("0") ? data.phone.slice(1) : data.phone;
const payload: SignupPayload = {
email: data.email,
username:
data.username,
phoneNumber:
fullPhoneNumber,
userType:
data.userType,
name: {
en: data.name.en,
am:
data.name.am ||
"",
},
username: data.username,
phoneNumber: `${data.countryCode}${normalizedPhone}`,
userType: data.userType,
name: { en: data.name.en, am: data.name.am ?? "" },
};
const res =
await createUserMutation.mutateAsync(
payload
);
if (res?.success) {
// save auth token
// document.cookie = `auth-token=${res.data?.token}; path=/`;
localStorage.setItem(
"auth-token",
`auth-token=${res.data?.token}; path=/`
);
localStorage.setItem(
"userId",res.data?.userId
);
localStorage.setItem(
"otp",res.data?.otp?.split(" ")?.[6]
);
createOTP({ phone: payload.phoneNumber, otp:res.data?.otp?.split(" ")?.[6] })
// save phone for otp page
localStorage.setItem(
"otp-phone",
payload.phoneNumber
);
// save phone for set password page
localStorage.setItem(
"otp-email",
payload.email
);
// navigate otp page
const result = await signup(payload);
if (result.success) {
navigate("/otp");
} else {
setError(result.error.message);
}
} catch (err) {
console.error(err);
} catch {
setError("An unexpected error occurred");
} finally {
setLoading(false);
}
};
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
return (
<div className="min-h-screen bg-background text-foreground">
<div className="grid min-h-screen lg:grid-cols-2">
{/* ------------------------------------------------------------------ */}
{/* Left Side */}
{/* ------------------------------------------------------------------ */}
<div className="relative hidden overflow-hidden bg-primary p-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">
{/* Logo */}
<div className="flex items-center gap-3">
<div className="flex size-14 items-center justify-center rounded-3xl bg-white/10 backdrop-blur">
<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>
</div>
</div>
{/* Hero */}
<div className="mt-20 max-w-lg">
<div className="inline-flex rounded-full bg-white/10 px-4 py-2 text-sm font-semibold backdrop-blur">
Smart Freight
Operations
</div>
<h2 className="mt-6 text-5xl font-black leading-tight tracking-tight">
Create your freight
operations account
</h2>
<p className="mt-6 text-lg leading-8 opacity-85">
Join EDR Freight to
manage shipments,
monitor railway
operations, track
consignments, and
streamline logistics
workflows across
Ethiopia and
Djibouti.
</p>
</div>
{/* Features */}
<div className="mt-14 grid gap-5">
{[
"Real-time shipment tracking",
"Secure logistics management",
"Enterprise-grade operations",
"Multi-corridor freight monitoring",
].map((item) => (
<div
key={item}
className="flex items-center gap-3"
>
<div className="flex size-10 items-center justify-center rounded-2xl bg-white/10">
<ShieldCheck className="size-5" />
</div>
<span className="font-medium">
{item}
</span>
</div>
))}
</div>
</div>
{/* Stats */}
<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">
Active Corridors
</p>
<h3 className="mt-2 text-4xl font-black">
24+
</h3>
</div>
<div className="rounded-2xl bg-white/10 px-4 py-2 text-sm font-semibold">
Operational
</div>
</div>
<div className="mt-6 h-3 overflow-hidden rounded-full bg-white/10">
<div className="h-full w-[95%] rounded-full bg-white" />
</div>
</div>
</div>
{/* ------------------------------------------------------------------ */}
{/* Right Side */}
{/* ------------------------------------------------------------------ */}
<div className="flex items-center justify-center p-6 md:p-10">
<div className="w-full max-w-2xl">
{/* Mobile Logo */}
<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>
</div>
</div>
{/* Form Card */}
<div className="rounded-[36px] border border-border bg-card p-8 shadow-2xl md:p-10">
{/* Header */}
<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>
<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.
</p>
</div>
{/* Success */}
{createUserMutation.isSuccess && (
<div className="mb-6 rounded-2xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
Account created
successfully.
</div>
)}
{/* Error */}
{createUserMutation.isError && (
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
Failed to create
account. Please try
again.
</div>
)}
{/* Form */}
<form
onSubmit={handleSubmit(
onSubmit
)}
className="space-y-6"
>
{/* Full Name */}
<div>
<label className="mb-2 block text-sm font-semibold">
Full Name
</label>
<input
type="text"
placeholder="John Doe"
disabled={
createUserMutation.isPending
}
{...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>
{/* Username */}
<div>
<label className="mb-2 block text-sm font-semibold">
Username
</label>
<input
type="text"
placeholder="john_doe"
disabled={
createUserMutation.isPending
}
{...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>
{/* Email */}
<div>
<label className="mb-2 block text-sm font-semibold">
Email Address
</label>
<input
type="email"
placeholder="john@example.com"
disabled={
createUserMutation.isPending
}
{...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>
{/* Phone */}
<div>
<label className="mb-2 block text-sm font-semibold">
Phone Number
</label>
<div className="flex gap-2">
<input
type="text"
disabled={
createUserMutation.isPending
}
{...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"
/>
<input
type="tel"
placeholder="912345678"
disabled={
createUserMutation.isPending
}
{...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>
{/* Submit */}
<button
type="submit"
disabled={
createUserMutation.isPending
}
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"
>
{createUserMutation.isPending ? (
"Creating..."
) : (
<>
Create Account
<ArrowRight className="size-5" />
</>
)}
</button>
{/* Footer */}
<p className="text-center text-sm text-muted-foreground">
Already have an
account?
<button
type="button"
className="ml-2 font-semibold text-primary hover:underline"
>
Sign In
</button>
</p>
</form>
</div>
</div>
<AuthLayout
left={{
badge: "Smart Freight Operations",
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%]" },
}}
>
<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>
<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.
</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">
{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
type="text"
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"
/>
<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>
<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"
>
{loading ? (
"Creating..."
) : (
<>
Create Account
<ArrowRight className="size-5" />
</>
)}
</button>
<p className="text-center text-sm text-muted-foreground">
Already have an account?
<button
type="button"
onClick={() => navigate("/login")}
className="ml-2 font-semibold text-primary hover:underline"
>
Sign In
</button>
</p>
</form>
</AuthLayout>
);
}
}

View File

@@ -1,70 +1,28 @@
import { verificationCodeType } from "@/enums/verificationCodeType";
import {
generateVerificationCode,
verifyOTP,
} from "@/services/account";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
ArrowRight,
ShieldCheck,
Train,
MailCheck,
RotateCw,
} from "lucide-react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// -----------------------------------------------------------------------------
// Schema
// -----------------------------------------------------------------------------
import { ArrowRight, MailCheck, RotateCw } from "lucide-react";
import { verificationCodeType } from "@/enums/verificationCodeType";
import useAuth from "@/hooks/useAuth";
import AuthLayout from "@/components/auth/AuthLayout";
const otpSchema = z.object({
code: z
.string()
.regex(
/^\d{6}$/,
"OTP must be exactly 6 digits"
),
code: z.string().regex(/^\d{6}$/, "OTP must be exactly 6 digits"),
});
type FormData = z.infer<
typeof otpSchema
>;
// -----------------------------------------------------------------------------
// Component
// -----------------------------------------------------------------------------
type FormData = z.infer<typeof otpSchema>;
export default function VerificationOtpPage() {
const navigate =
useNavigate();
const navigate = useNavigate();
const { verifyOTP, generateVerificationCode } = useAuth();
const [verifying, setVerifying] = useState(false);
const [resending, setResending] = useState(false);
const [error, setError] = useState<string | null>(null);
const [resentMessage, setResentMessage] = useState<string | null>(null);
// ---------------------------------------------------------------------------
// Local Storage Data
// ---------------------------------------------------------------------------
const phone =
localStorage.getItem(
"otp-phone"
) || "";
const email =
localStorage.getItem(
"otp-email"
) || "";
// ---------------------------------------------------------------------------
// Form
// ---------------------------------------------------------------------------
const phone = localStorage.getItem("otp-phone") || "";
const {
register,
@@ -72,383 +30,152 @@ export default function VerificationOtpPage() {
formState: { errors },
watch,
} = useForm<FormData>({
resolver:
zodResolver(otpSchema),
defaultValues: {
code: "",
},
resolver: zodResolver(otpSchema),
defaultValues: { code: "" },
});
const otpValue =
watch("code");
const otpValue = watch("code");
// ---------------------------------------------------------------------------
// Verify Mutation
// ---------------------------------------------------------------------------
const verifyMutation =
useMutation({
mutationFn: async (
data: {
phone: string;
otp: string;
}
) => verifyOTP(data),
onSuccess: () => {
navigate(
"/set-password"
);
},
});
// ---------------------------------------------------------------------------
// Resend Mutation
// ---------------------------------------------------------------------------
const resendMutation =
useMutation({
mutationFn: async () => {
return generateVerificationCode(
{
email,
phoneNumber:
phone,
type:
verificationCodeType.setPassword,
}
);
},
});
// ---------------------------------------------------------------------------
// Submit
// ---------------------------------------------------------------------------
const onSubmit = async (
data: FormData
) => {
const onSubmit = async (data: FormData) => {
setError(null);
setVerifying(true);
try {
await verifyMutation.mutateAsync(
{
phone,
otp: data.code,
}
);
} catch (err) {
console.error(err);
const result = await verifyOTP(data.code);
if (result.success) {
navigate("/set-password");
} else {
setError(result.error.message);
}
} catch {
setError("An unexpected error occurred");
} finally {
setVerifying(false);
}
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const handleResend = async () => {
setResentMessage(null);
setResending(true);
try {
const result = await generateVerificationCode(verificationCodeType.setPassword);
if (result.success) {
setResentMessage("New OTP code sent successfully.");
} else {
setError(result.error.message);
}
} catch {
setError("An unexpected error occurred");
} finally {
setResending(false);
}
};
const maskedPhone =
phone.length > 4
? `${phone.slice(
0,
7
)}******`
: phone;
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
const maskedPhone = phone.length > 4 ? `${phone.slice(0, 7)}******` : phone;
return (
<div className="min-h-screen bg-background text-foreground">
<div className="grid min-h-screen lg:grid-cols-2">
{/* ------------------------------------------------------------------ */}
{/* Left Side */}
{/* ------------------------------------------------------------------ */}
<div className="relative hidden overflow-hidden bg-primary p-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">
{/* Logo */}
<div className="flex items-center gap-3">
<div className="flex size-14 items-center justify-center rounded-3xl bg-white/10 backdrop-blur">
<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>
</div>
</div>
{/* Hero */}
<div className="mt-20 max-w-lg">
<div className="inline-flex rounded-full bg-white/10 px-4 py-2 text-sm font-semibold backdrop-blur">
Secure
Verification
</div>
<h2 className="mt-6 text-5xl font-black leading-tight tracking-tight">
Verify your
account securely
</h2>
<p className="mt-6 text-lg leading-8 opacity-85">
Enter the
verification code
sent to your phone
number to continue
using EDR Freight
logistics services.
</p>
</div>
{/* Features */}
<div className="mt-14 grid gap-5">
{[
"Secure OTP verification",
"Protected account access",
"Fast identity confirmation",
"Enterprise-grade security",
].map((item) => (
<div
key={item}
className="flex items-center gap-3"
>
<div className="flex size-10 items-center justify-center rounded-2xl bg-white/10">
<ShieldCheck className="size-5" />
</div>
<span className="font-medium">
{item}
</span>
</div>
))}
</div>
</div>
{/* Footer Stats */}
<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">
Verification
Security
</p>
<h3 className="mt-2 text-4xl font-black">
99.9%
</h3>
</div>
<div className="rounded-2xl bg-white/10 px-4 py-2 text-sm font-semibold">
Protected
</div>
</div>
<div className="mt-6 h-3 overflow-hidden rounded-full bg-white/10">
<div className="h-full w-[99%] rounded-full bg-white" />
</div>
</div>
<AuthLayout
left={{
badge: "Secure Verification",
title: "Verify your account securely",
description:
"Enter the verification code sent to your phone number to continue using EDR Freight logistics services.",
features: [
"Secure OTP verification",
"Protected account access",
"Fast identity confirmation",
"Enterprise-grade security",
],
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>
{/* ------------------------------------------------------------------ */}
{/* Right Side */}
{/* ------------------------------------------------------------------ */}
<div className="flex items-center justify-center p-6 md:p-10">
<div className="w-full max-w-xl">
{/* Mobile Logo */}
<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>
</div>
</div>
{/* OTP Card */}
<div className="rounded-[36px] border border-border bg-card p-8 shadow-2xl md:p-10">
{/* Header */}
<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>
<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>
</div>
</div>
{/* Success */}
{verifyMutation.isSuccess && (
<div className="mb-6 rounded-2xl border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
Verification
successful.
</div>
)}
{/* Error */}
{verifyMutation.isError && (
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
Invalid OTP
code. Please try
again.
</div>
)}
{/* Resend Success */}
{resendMutation.isSuccess && (
<div className="mb-6 rounded-2xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-700">
New OTP code sent
successfully.
</div>
)}
{/* Form */}
<form
onSubmit={handleSubmit(
onSubmit
)}
className="space-y-6"
>
{/* OTP */}
<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={
verifyMutation.isPending
}
{...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>
{/* Verify Button */}
<button
type="submit"
disabled={
verifyMutation.isPending ||
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"
>
{verifyMutation.isPending ? (
"Verifying..."
) : (
<>
Verify Account
<ArrowRight className="size-5" />
</>
)}
</button>
{/* Resend */}
<button
type="button"
onClick={() =>
resendMutation.mutate()
}
disabled={
resendMutation.isPending
}
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"
>
{resendMutation.isPending ? (
"Sending..."
) : (
<>
<RotateCw className="size-5" />
Resend Code
</>
)}
</button>
{/* Footer */}
<p className="text-center text-sm text-muted-foreground">
Didnt receive
the code?
<button
type="button"
onClick={() =>
resendMutation.mutate()
}
className="ml-2 font-semibold text-primary hover:underline"
>
Send again
</button>
</p>
</form>
</div>
</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>
</div>
</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">
{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">
{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>
<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"
>
{verifying ? (
"Verifying..."
) : (
<>
Verify Account
<ArrowRight className="size-5" />
</>
)}
</button>
<button
type="button"
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"
>
{resending ? (
"Sending..."
) : (
<>
<RotateCw className="size-5" />
Resend Code
</>
)}
</button>
<p className="text-center text-sm text-muted-foreground">
Didn't receive the code?
<button
type="button"
onClick={handleResend}
className="ml-2 font-semibold text-primary hover:underline"
>
Send again
</button>
</p>
</form>
</AuthLayout>
);
}
}