v2.0.0: Complete rewrite — React + Express + PostgreSQL + Docker

BREAKING: Full rewrite from static HTML to production-grade stack.

Features:
- React 18 + Vite + Tailwind CSS (mobile-first)
- Express + Prisma + PostgreSQL backend
- JWT authentication with role-based access
- Weekly Mon-Sun timesheets with auto-save
- Multiple homeowner entries per day
- Submit → Approve/Reject workflow
- Server-side PDF generation
- SMTP email integration
- Admin panel with reporting & filters
- Dark mode (system-aware)
- Docker Compose one-command deploy
- Non-root containers, Helmet, bcrypt, Zod validation
This commit is contained in:
BizzleBot
2026-02-15 20:16:46 +00:00
parent 68832bd958
commit a7c138add1
88 changed files with 5522 additions and 9159 deletions
+124
View File
@@ -0,0 +1,124 @@
import { Outlet, NavLink, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import ThemeToggle from './ThemeToggle';
import { Calendar, Clock, Shield, LogOut, Menu, X } 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' },
...(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>
);
}