Add theme system with color presets, fix login page styling, persist auth session

Theme system with 5 color presets (Slate, Emerald, Violet, Amber, Rose)
and light/dark/system mode. User menu in sidebar with theme picker and
sign out. Login page uses standalone dark branded styling with autofill
override. Auth persists in sessionStorage across refreshes.
This commit is contained in:
Ryan Moon
2026-03-28 08:30:24 -05:00
parent 9abdf6c050
commit 7c64a928e1
10 changed files with 691 additions and 145 deletions

View File

@@ -1,5 +1,20 @@
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router'
import { useAuthStore } from '@/stores/auth.store'
import { useThemeStore } from '@/stores/theme.store'
import { themes } from '@/lib/themes'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from '@/components/ui/dropdown-menu'
import { Users, Sun, Moon, Monitor, LogOut, User, Palette } from 'lucide-react'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: () => {
@@ -11,15 +26,102 @@ export const Route = createFileRoute('/_authenticated')({
component: AuthenticatedLayout,
})
function ModeIcon() {
const mode = useThemeStore((s) => s.mode)
if (mode === 'dark') return <Moon className="h-4 w-4" />
if (mode === 'light') return <Sun className="h-4 w-4" />
return <Monitor className="h-4 w-4" />
}
function AuthenticatedLayout() {
const router = useRouter()
const user = useAuthStore((s) => s.user)
const logout = useAuthStore((s) => s.logout)
const { mode, colorTheme, setMode, setColorTheme } = useThemeStore()
function handleLogout() {
logout()
router.navigate({ to: '/login', replace: true })
}
return (
<div className="min-h-screen bg-background text-foreground">
<div className="flex">
<nav className="w-56 border-r border-border bg-sidebar min-h-screen p-4 space-y-2">
<h2 className="text-lg font-semibold text-sidebar-foreground mb-4">Forte</h2>
<a href="/accounts" className="block px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent">
Accounts
</a>
<nav className="w-56 border-r border-border bg-sidebar min-h-screen flex flex-col">
<div className="p-4">
<h2 className="text-lg font-semibold text-sidebar-foreground">Forte</h2>
</div>
<div className="flex-1 px-3 space-y-1">
<Link
to="/accounts"
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
>
<Users className="h-4 w-4" />
Accounts
</Link>
</div>
<div className="p-3 border-t border-sidebar-border">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-full justify-start gap-2 text-sm text-sidebar-foreground">
<User className="h-4 w-4" />
<span className="truncate">{user?.firstName} {user?.lastName}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel className="font-normal">
<p className="text-sm font-medium">{user?.firstName} {user?.lastName}</p>
<p className="text-xs text-muted-foreground">{user?.email}</p>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<ModeIcon />
<span className="ml-2">Mode</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => setMode('light')}>
<Sun className="mr-2 h-4 w-4" />
Light {mode === 'light' && '•'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setMode('dark')}>
<Moon className="mr-2 h-4 w-4" />
Dark {mode === 'dark' && '•'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setMode('system')}>
<Monitor className="mr-2 h-4 w-4" />
System {mode === 'system' && '•'}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette className="h-4 w-4" />
<span className="ml-2">Color</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{themes.map((t) => (
<DropdownMenuItem key={t.name} onClick={() => setColorTheme(t.name)}>
<span
className="mr-2 h-4 w-4 rounded-full border inline-block"
style={{ backgroundColor: `hsl(${t.light.primary})` }}
/>
{t.label} {colorTheme === t.name && '•'}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</nav>
<main className="flex-1 p-6">
<Outlet />

View File

@@ -1,18 +1,20 @@
import { useState } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { createFileRoute, useRouter, redirect } from '@tanstack/react-router'
import { useAuthStore } from '@/stores/auth.store'
import { login } from '@/api/auth'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export const Route = createFileRoute('/login')({
beforeLoad: () => {
const { token } = useAuthStore.getState()
if (token) {
throw redirect({ to: '/accounts' })
}
},
component: LoginPage,
})
function LoginPage() {
const navigate = useNavigate()
const router = useRouter()
const setAuth = useAuthStore((s) => s.setAuth)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
@@ -27,7 +29,8 @@ function LoginPage() {
try {
const res = await login(email, password)
setAuth(res.token, res.user)
navigate({ to: '/accounts' })
await router.invalidate()
await router.navigate({ to: '/accounts', replace: true })
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed')
} finally {
@@ -36,44 +39,55 @@ function LoginPage() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Forte</CardTitle>
<p className="text-sm text-muted-foreground">Sign in to your account</p>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Signing in...' : 'Sign in'}
</Button>
</form>
</CardContent>
</Card>
<div
className="flex min-h-screen items-center justify-center"
style={{ background: 'linear-gradient(135deg, #0f1724 0%, #142038 100%)' }}
>
<div
className="w-full max-w-sm rounded-xl border p-8 shadow-2xl"
style={{ backgroundColor: '#131c2e', borderColor: '#1e2d45' }}
>
<div className="text-center mb-8">
<h1 className="text-3xl font-bold" style={{ color: '#d8dfe9' }}>Forte</h1>
<p className="text-sm mt-1" style={{ color: '#6b7a8d' }}>Music Store Management</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium" style={{ color: '#b0bec5' }}>Email</label>
<input
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="h-9 w-full rounded-md border px-3 py-1 text-sm outline-none login-input"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium" style={{ color: '#b0bec5' }}>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="h-9 w-full rounded-md border px-3 py-1 text-sm outline-none login-input"
/>
</div>
{error && (
<p className="text-sm" style={{ color: '#e57373' }}>{error}</p>
)}
<button
type="submit"
disabled={loading}
className="h-9 w-full rounded-md border text-sm font-medium transition-colors disabled:opacity-50"
style={{ backgroundColor: 'transparent', color: '#d0d8e0', borderColor: '#3a4a62' }}
onMouseEnter={(e) => { (e.target as HTMLElement).style.backgroundColor = '#1e2d45' }}
onMouseLeave={(e) => { (e.target as HTMLElement).style.backgroundColor = 'transparent' }}
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</form>
</div>
</div>
)
}