mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
223 lines
8.8 KiB
TypeScript
223 lines
8.8 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
Home,
|
|
Phone,
|
|
Ticket,
|
|
HelpCircle,
|
|
Moon,
|
|
Sun,
|
|
KeyRound,
|
|
LogOut,
|
|
ChevronDown,
|
|
Check,
|
|
} from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import dynamic from 'next/dynamic';
|
|
import { usePathname } from 'next/navigation';
|
|
import { useEffect, useState } from 'react';
|
|
import { useAuthStore } from '@/lib/auth-store';
|
|
import { BOOKING_STEPS } from '@/components/ProgressIndicator';
|
|
|
|
// AppSidebar renders on every page via the root layout, so anything imported
|
|
// here ships to every visitor's first load — but this modal is only ever
|
|
// reachable by an already-authenticated user opening the account dropdown.
|
|
// Code-split it out instead of paying for it on every page/every visitor.
|
|
const ChangePasswordModal = dynamic(() => import('@/components/ChangePasswordModal'), { ssr: false });
|
|
|
|
// Mirrors booking/layout.tsx's stepMap — the linear booking flow routes that
|
|
// get a vertical step list instead of the standard nav highlighting.
|
|
const BOOKING_STEP_MAP: Record<string, string> = {
|
|
'/booking/search': 'search',
|
|
'/booking/results': 'results',
|
|
'/booking/auth-check': 'passengers',
|
|
'/booking/passengers': 'passengers',
|
|
'/booking/seats': 'seats',
|
|
'/booking/review': 'review',
|
|
'/booking/payment': 'payment',
|
|
'/booking/confirmation': 'confirmation',
|
|
};
|
|
|
|
const NAV_LINKS = [
|
|
{ href: '/', label: 'Home', icon: Home },
|
|
{ href: '/booking/lookup', label: 'My Bookings', icon: Ticket },
|
|
{ href: '/contact', label: 'Contact', icon: Phone },
|
|
{ href: '/help', label: 'Help', icon: HelpCircle },
|
|
];
|
|
|
|
export default function AppSidebar() {
|
|
const pathname = usePathname();
|
|
const [isDark, setIsDark] = useState(false);
|
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
const [showChangePassword, setShowChangePassword] = useState(false);
|
|
const { user, isAuthenticated, initialize, logout } = useAuthStore();
|
|
|
|
useEffect(() => {
|
|
setIsDark(document.documentElement.classList.contains('dark'));
|
|
initialize();
|
|
}, [initialize]);
|
|
|
|
const toggleTheme = () => {
|
|
const html = document.documentElement;
|
|
const nextDark = !html.classList.contains('dark');
|
|
html.classList.toggle('dark', nextDark);
|
|
localStorage.setItem('theme', nextDark ? 'dark' : 'light');
|
|
setIsDark(nextDark);
|
|
};
|
|
|
|
const currentStepId = BOOKING_STEP_MAP[pathname ?? ''];
|
|
const currentStepIndex = currentStepId ? BOOKING_STEPS.findIndex((s) => s.id === currentStepId) : -1;
|
|
|
|
return (
|
|
<aside className="hidden lg:flex w-64 fixed inset-y-0 left-0 z-40 flex-col bg-[rgb(20_113_76)] dark:bg-gray-900 border-r border-[rgb(16_89_60)] dark:border-gray-800">
|
|
<Link href="/" className="flex items-center px-5 h-20 flex-shrink-0 hover:opacity-80 transition-opacity">
|
|
<Image
|
|
src="/edr-logo.png"
|
|
alt="Ethio-Djibouti Railway"
|
|
width={140}
|
|
height={48}
|
|
className="h-16 w-auto"
|
|
priority
|
|
/>
|
|
</Link>
|
|
|
|
<nav className="flex-1 overflow-y-auto px-3 py-4 space-y-1">
|
|
{NAV_LINKS.map(({ href, label, icon: Icon }) => {
|
|
const isActive = href === '/' ? pathname === '/' : pathname?.startsWith(href);
|
|
return (
|
|
<Link
|
|
key={href}
|
|
href={href}
|
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors ${
|
|
isActive
|
|
? 'bg-white/15 text-white'
|
|
: 'text-gray-100 hover:bg-white/10 hover:text-white'
|
|
}`}
|
|
>
|
|
<Icon className="w-5 h-5" />
|
|
{label}
|
|
</Link>
|
|
);
|
|
})}
|
|
|
|
{currentStepIndex >= 0 && (
|
|
<div className="mt-6 pt-4 border-t border-white/15">
|
|
<p className="px-3 pb-2 text-xs font-semibold uppercase tracking-wide text-gray-200/80">
|
|
Your booking
|
|
</p>
|
|
<ol className="space-y-1">
|
|
{BOOKING_STEPS.map((step, index) => {
|
|
const isComplete = index < currentStepIndex;
|
|
const isCurrent = index === currentStepIndex;
|
|
return (
|
|
<li key={step.id} className="flex items-center gap-2.5 px-3 py-1.5">
|
|
<span
|
|
className={`flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full text-[10px] font-bold transition-colors ${
|
|
isComplete
|
|
? 'bg-white text-[rgb(20,113,76)]'
|
|
: isCurrent
|
|
? 'border-2 border-white text-white'
|
|
: 'border border-white/30 text-white/50'
|
|
}`}
|
|
>
|
|
{isComplete ? <Check className="w-3 h-3" /> : index + 1}
|
|
</span>
|
|
<span
|
|
className={`text-sm ${
|
|
isCurrent ? 'text-white font-semibold' : isComplete ? 'text-gray-100' : 'text-white/50'
|
|
}`}
|
|
>
|
|
{step.name}
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</div>
|
|
)}
|
|
</nav>
|
|
|
|
<div className="flex-shrink-0 border-t border-white/15 p-3 space-y-1">
|
|
<button
|
|
onClick={toggleTheme}
|
|
className="flex w-full items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-gray-100 hover:bg-white/10 hover:text-white transition-colors"
|
|
>
|
|
{isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
|
{isDark ? 'Light mode' : 'Dark mode'}
|
|
</button>
|
|
|
|
{isAuthenticated && user ? (
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setShowUserMenu((v) => !v)}
|
|
className="flex w-full items-center gap-3 px-3 py-2 rounded-lg hover:bg-white/10 transition-colors"
|
|
>
|
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-white text-[rgb(20_113_76)] font-semibold text-sm">
|
|
{user.fullName?.toUpperCase().charAt(0) || 'U'}
|
|
</div>
|
|
<span className="flex-1 text-left text-sm font-medium text-white truncate">{user.fullName}</span>
|
|
<ChevronDown className={`w-4 h-4 text-gray-200 transition-transform ${showUserMenu ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
|
|
{showUserMenu && (
|
|
<div className="absolute bottom-full left-0 mb-2 w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-xl overflow-hidden">
|
|
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">{user.fullName}</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{user.email}</p>
|
|
</div>
|
|
<div className="p-2">
|
|
<button
|
|
onClick={() => {
|
|
setShowUserMenu(false);
|
|
setShowChangePassword(true);
|
|
}}
|
|
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
>
|
|
<KeyRound className="h-4 w-4" />
|
|
Change Password
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setShowUserMenu(false);
|
|
logout();
|
|
}}
|
|
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-[rgb(20_113_76)] dark:text-emerald-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
// TODO: Sign in / Register temporarily disabled — re-enable later.
|
|
null
|
|
/* <div className="flex items-center gap-2 px-1 pt-1">
|
|
<Link
|
|
href="/login"
|
|
className="flex-1 text-center px-3 py-2 text-sm font-medium text-gray-100 hover:bg-white/10 rounded-lg transition-colors"
|
|
>
|
|
Sign in
|
|
</Link>
|
|
<Link
|
|
href="/register"
|
|
className="flex-1 text-center px-3 py-2 text-sm font-medium bg-white text-[rgb(20_113_76)] hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
Register
|
|
</Link>
|
|
</div> */
|
|
)}
|
|
</div>
|
|
|
|
{showChangePassword && (
|
|
<ChangePasswordModal
|
|
isOpen={showChangePassword}
|
|
onClose={() => setShowChangePassword(false)}
|
|
/>
|
|
)}
|
|
</aside>
|
|
);
|
|
}
|