feat(portal): Overhaul authentication flow and application routing

This commit is contained in:
ghost2023
2026-06-01 10:43:09 +03:00
parent 243a18e3e3
commit 0aece8f1dc
3 changed files with 89 additions and 57 deletions

View File

@@ -4,13 +4,13 @@ import {
Routes,
Route,
Navigate,
Outlet,
} from "react-router-dom";
import { DashboardLayout, type SidebarItem } from "@edr/ui-common";
import {
CalendarCheck,
MapPin,
Receipt,
FileText,
Home,
Loader2,
User,
@@ -36,7 +36,7 @@ import CustomerOnBoarding from "./pages/customers/on_boarding/TransportrOnBoardi
import CustomerOnboardingPage from "./pages/customers/on_boarding/CustomerOnboardingPage";
const sidebarItems: SidebarItem[] = [
{ label: "Home", href: "/", icon: <Home /> },
{ label: "Home", href: "/portal", icon: <Home /> },
{ label: "My Bookings", href: "/bookings", icon: <CalendarCheck /> },
{ label: "Tracking", href: "/tracking", icon: <MapPin /> },
{ label: "Billing", href: "/billing", icon: <Receipt /> },
@@ -46,13 +46,20 @@ const sidebarItems: SidebarItem[] = [
const App = () => {
const navigate = useNavigate();
const location = useLocation();
const { user, isPending, logout, customer, customerQuery } = useAuth();
console.log({ customer, isPending, user });
const { user, isPending, logout, customer } = useAuth();
useEffect(() => {
if (!user) return;
// if (!user.hasSetPassword) navigate("/set-password");
}, [user]);
if (isPending) return;
const isInProtectedRoutes = sidebarItems.find((item) =>
location.pathname.startsWith(item.href),
);
if (!user) {
if (isInProtectedRoutes) return navigate("/login");
return;
}
if (!customer && !!isInProtectedRoutes) navigate("/onboarding");
else if (customer && !isInProtectedRoutes) return navigate("/portal");
}, [user, location, customer]);
if (isPending) {
return (
@@ -62,50 +69,45 @@ const App = () => {
);
}
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="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
if (user && !customer && !customerQuery.isPending) {
return <OnboardingPage />;
}
return <CustomerOnboardingPage />
const displayName = user?.name?.en || user?.username || user?.email || "User";
const userEmail = user?.email;
return (
<DashboardLayout
title="EDR Freight"
sidebarItems={sidebarItems}
activeHref={location.pathname}
onNavigate={navigate}
enableThemeToggle
userName={displayName}
userEmail={userEmail}
onLogout={logout}
>
<Routes>
<Route path="/" element={<MyPortalPage />} />
<Routes>
<Route>
<Route index 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="/onboarding" element={<OnboardingPage />} />
</Route>
<Route
element={
<DashboardLayout
title="EDR Freight"
sidebarItems={sidebarItems}
activeHref={location.pathname}
onNavigate={navigate}
enableThemeToggle
userName={displayName}
userEmail={userEmail}
onLogout={logout}
>
<Outlet />
</DashboardLayout>
}
>
<Route path="/portal" element={<MyPortalPage />} />
<Route path="/bookings" element={<MyBookings />} />
<Route path="/bookings/new" element={<NewBookingPage />} />
<Route path="/bookings/:id" element={<BookingDetailPage />} />
<Route path="/tracking" element={<TrackingPage />} />
<Route path="/billing" element={<BillingPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</DashboardLayout>
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
};

View File

@@ -9,6 +9,7 @@ import type {
} from "@/types/auth";
import type { Result } from "@/utils/result";
import { extractApiError } from "@/utils/result";
import { useEffect } from "react";
function setCookie(name: string, value: string, days: number) {
const expires = new Date();
@@ -44,6 +45,16 @@ const useAuth = () => {
}),
);
useEffect(() => {
console.log({
user: authQuery.data,
customer: customerQuery.data,
isCustomer: !!customerQuery.data,
isUserPending: authQuery.isPending,
isCustomerPending: customerQuery.isPending,
});
}, [authQuery, customerQuery]);
const hasToken = !!getCookie("auth-token");
const isPending = authQuery.isPending && hasToken;

View File

@@ -26,7 +26,11 @@ const userSchema = z.object({
.min(9, "Phone number is too short")
.max(9, "Phone number is too long"),
userType: z.string(),
name: z.object({
firstName: z.object({
en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
lastName: z.object({
en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
@@ -51,7 +55,8 @@ export default function SignupPage() {
countryCode: "+251",
phone: "",
userType: userType.individual,
name: { en: "", am: "" },
firstName: { en: "", am: "" },
lastName: { en: "", am: "" },
},
});
@@ -67,11 +72,12 @@ export default function SignupPage() {
username: data.email,
phoneNumber: `${data.countryCode}${normalizedPhone}`,
userType: data.userType,
name: { en: data.name.en, am: data.name.am ?? "" },
name: { en: `${data.firstName.en} ${data.lastName.en}`, am: "" },
};
const result = await signup(payload);
if (result.success) {
navigate("/otp");
navigate("/portal");
// navigate("/otp");
} else {
setError(result.error.message);
}
@@ -121,18 +127,31 @@ export default function SignupPage() {
<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}
aria-invalid={Boolean(errors.name?.en)}
{...register("name.en")}
/>
<FieldError errors={[errors.name?.en]} />
</Field>
<div className="flex gap-4">
<Field data-invalid={Boolean(errors.firstName?.en)}>
<FieldLabel>First Name</FieldLabel>
<Input
type="text"
placeholder="John"
disabled={loading}
aria-invalid={Boolean(errors.firstName?.en)}
{...register("firstName.en")}
/>
<FieldError errors={[errors.firstName?.en]} />
</Field>
<Field data-invalid={Boolean(errors.lastName?.en)}>
<FieldLabel>Last Name</FieldLabel>
<Input
type="text"
placeholder="Doe"
disabled={loading}
aria-invalid={Boolean(errors.lastName?.en)}
{...register("lastName.en")}
/>
<FieldError errors={[errors.lastName?.en]} />
</Field>
</div>
<Field data-invalid={Boolean(errors.email)}>
<FieldLabel>Email Address</FieldLabel>
<Input