Implement shadcn/ui + Tailwind CSS UI layer

- Design token system via CSS custom properties (light/dark mode)
- Theme context hook + ThemeToggle component
- AppShell + collapsible Sidebar replace inline Nav
- LoginPage redesigned: two-column grid with hero panel
- shadcn/ui Button and Input components
- Tailwind config wired to CSS variable tokens
- All pages de-Nav'd; PrivateRoute/AdminRoute wrap with AppShell
- TypeScript passes clean (npm run typecheck)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-17 12:32:06 +02:00
parent 9e2e4ec338
commit c3f87706ee
26 changed files with 1263 additions and 89 deletions
+133
View File
@@ -0,0 +1,133 @@
import { useState } from "react";
import { NavLink } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import {
Home,
Grid2X2,
Settings,
ShieldCheck,
ChevronRight,
ChevronLeft,
LogOut,
UserCircle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import ThemeToggle from "@/components/ThemeToggle";
import { useAuth } from "@/hooks/useAuth";
import { getMe } from "@/api/client";
import { cn } from "@/lib/utils";
const NAV_ITEMS = [
{ to: "/", label: "Home", icon: Home, exact: true },
{ to: "/apps", label: "Apps", icon: Grid2X2, exact: false },
{ to: "/settings", label: "Settings", icon: Settings, exact: false },
];
export default function Sidebar() {
const [expanded, setExpanded] = useState(true);
const { logout } = useAuth();
const { data: user } = useQuery({ queryKey: ["me"], queryFn: getMe });
return (
<aside
className={cn(
"flex flex-col h-screen bg-surface border-r border-border transition-all duration-200 shrink-0",
expanded ? "w-56" : "w-16"
)}
>
{/* Nav items */}
<nav className="flex-1 py-4 px-2 space-y-1 overflow-hidden">
{NAV_ITEMS.map(({ to, label, icon: Icon, exact }) => (
<NavLink
key={to}
to={to}
end={exact}
className={({ isActive }) =>
cn(
"flex items-center rounded-lg transition-colors",
expanded ? "px-3 py-2 gap-3" : "justify-center py-3",
isActive
? "bg-primary/10 text-primary"
: "text-muted hover:bg-muted/20 hover:text-foreground"
)
}
>
<Icon className="h-5 w-5 shrink-0" />
{expanded && (
<span className="text-sm font-medium whitespace-nowrap">{label}</span>
)}
</NavLink>
))}
{user?.is_admin && (
<NavLink
to="/admin"
className={({ isActive }) =>
cn(
"flex items-center rounded-lg transition-colors",
expanded ? "px-3 py-2 gap-3" : "justify-center py-3",
isActive
? "bg-primary/10 text-primary"
: "text-muted hover:bg-muted/20 hover:text-foreground"
)
}
>
<ShieldCheck className="h-5 w-5 shrink-0" />
{expanded && (
<span className="text-sm font-medium whitespace-nowrap">Admin</span>
)}
</NavLink>
)}
</nav>
{/* Bottom section */}
<div className="py-4 px-2 space-y-1 border-t border-border">
{/* User avatar row */}
<div
className={cn(
"flex items-center rounded-lg py-2 text-muted",
expanded ? "px-3 gap-3" : "justify-center"
)}
>
<UserCircle className="h-5 w-5 shrink-0" />
{expanded && user && (
<span className="text-sm font-medium truncate">{user.email}</span>
)}
</div>
{/* Theme toggle */}
<div className={cn("flex", expanded ? "px-1" : "justify-center")}>
<ThemeToggle />
</div>
{/* Logout */}
<Button
variant="ghost"
className={cn(
"w-full text-muted hover:text-foreground",
expanded ? "justify-start px-3 gap-3" : "justify-center px-0"
)}
onClick={logout}
>
<LogOut className="h-5 w-5 shrink-0" />
{expanded && <span className="text-sm font-medium">Logout</span>}
</Button>
{/* Collapse toggle */}
<Button
variant="ghost"
size="icon"
className={cn("w-full text-muted hover:text-foreground", !expanded && "px-0")}
onClick={() => setExpanded((e) => !e)}
aria-label={expanded ? "Collapse sidebar" : "Expand sidebar"}
>
{expanded ? (
<ChevronLeft className="h-5 w-5" />
) : (
<ChevronRight className="h-5 w-5" />
)}
</Button>
</div>
</aside>
);
}