Files
edr-platform/apps/edr-passenger-web/portal/src/components/AppSidebar.tsx
2026-07-14 12:01:36 +03:00

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>
);
}