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