Files
coastal_timesheet/frontend/src/components/Layout.jsx
T
rootandClaude Opus 4.6 2f381ff5f9 feat: user profile self-service + admin password reset
- Add PUT /api/auth/profile for users to change their own email/password
- Add POST /api/admin/users/:id/reset-password for admin password resets
- Add updateProfileSchema and resetPasswordSchema validation (Zod)
- Create Profile.jsx page with email change and password change forms
- Add Reset Password button with inline form to ManageUsers in Admin panel
- Add /profile route and Account nav link in Layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:00:58 +00:00

126 lines
5.2 KiB
React

import { Outlet, NavLink, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import ThemeToggle from './ThemeToggle';
import { Calendar, Clock, Shield, LogOut, Menu, X, User } from 'lucide-react';
import { useState } from 'react';
export default function Layout() {
const { user, logout, isAdmin } = useAuth();
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();
const navItems = [
{ to: '/', icon: Clock, label: 'Timesheet' },
{ to: '/history', icon: Calendar, label: 'History' },
{ to: '/profile', icon: User, label: 'Account' },
...(isAdmin ? [{ to: '/admin', icon: Shield, label: 'Admin' }] : []),
];
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-950 transition-colors">
{/* Header */}
<header className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl border-b border-gray-200/60 dark:border-gray-800/60">
<div className="max-w-5xl mx-auto px-4 h-14 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-lg font-semibold bg-gradient-to-r from-sky-500 to-teal-400 bg-clip-text text-transparent">
Coastal
</span>
</div>
{/* Desktop Nav */}
<nav className="hidden sm:flex items-center gap-1">
{navItems.map(({ to, icon: Icon, label }) => (
<NavLink
key={to}
to={to}
end={to === '/'}
className={({ isActive }) =>
`flex items-center gap-2 px-3 py-2 rounded-xl text-sm font-medium transition-all ${
isActive
? 'bg-sky-50 dark:bg-sky-500/10 text-sky-600 dark:text-sky-400'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'
}`
}
>
<Icon size={18} />
{label}
</NavLink>
))}
</nav>
<div className="flex items-center gap-2">
<ThemeToggle />
<div className="hidden sm:flex items-center gap-2 pl-2 border-l border-gray-200 dark:border-gray-700">
<span className="text-sm text-gray-600 dark:text-gray-400 max-w-[120px] truncate">{user?.name}</span>
<button onClick={logout} className="p-2 rounded-xl text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-all" title="Logout">
<LogOut size={18} />
</button>
</div>
<button onClick={() => setMenuOpen(!menuOpen)} className="sm:hidden p-2 rounded-xl text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800">
{menuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
</div>
{/* Mobile Menu */}
{menuOpen && (
<div className="sm:hidden border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 px-4 py-3 space-y-1">
{navItems.map(({ to, icon: Icon, label }) => (
<NavLink
key={to}
to={to}
end={to === '/'}
onClick={() => setMenuOpen(false)}
className={({ isActive }) =>
`flex items-center gap-3 px-4 py-3 rounded-xl text-base font-medium transition-all ${
isActive
? 'bg-sky-50 dark:bg-sky-500/10 text-sky-600 dark:text-sky-400'
: 'text-gray-600 dark:text-gray-400'
}`
}
>
<Icon size={20} />
{label}
</NavLink>
))}
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-100 dark:border-gray-800 mt-2">
<span className="text-sm text-gray-500 dark:text-gray-400">{user?.name} ({user?.role})</span>
<button onClick={logout} className="flex items-center gap-2 text-red-500 text-sm font-medium">
<LogOut size={16} /> Logout
</button>
</div>
</div>
)}
</header>
{/* Mobile Bottom Nav */}
<nav className="sm:hidden fixed bottom-0 left-0 right-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl border-t border-gray-200/60 dark:border-gray-800/60 safe-area-bottom">
<div className="flex justify-around py-2">
{navItems.map(({ to, icon: Icon, label }) => (
<NavLink
key={to}
to={to}
end={to === '/'}
className={({ isActive }) =>
`flex flex-col items-center gap-0.5 px-4 py-1.5 rounded-xl transition-all min-w-[64px] ${
isActive
? 'text-sky-500 dark:text-sky-400'
: 'text-gray-400 dark:text-gray-500'
}`
}
>
<Icon size={22} />
<span className="text-[10px] font-medium">{label}</span>
</NavLink>
))}
</div>
</nav>
{/* Content */}
<main className="max-w-5xl mx-auto px-4 py-6 pb-24 sm:pb-6">
<Outlet />
</main>
</div>
);
}