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:
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user