From 234156feedb2a7b5290edd9cffe1150640d0ad80 Mon Sep 17 00:00:00 2001 From: "XPS\\Micro" Date: Sun, 1 Feb 2026 23:13:03 +0100 Subject: [PATCH] feat: add shadcn sidebar to dashboard with navigation Integrate shadcn sidebar component into dashboard layout: - Create new dashboard/layout.tsx with SidebarProvider and responsive design - Add AppSidebar component with navigation items (Dashboard, Settings, Admin) - Implement responsive sidebar (permanent on desktop, drawer on mobile) - Add Settings page with user profile information - Update dashboard page to remove old header and integrate with new layout - Configure Tailwind CSS colors for sidebar theming - Add components.json for shadcn configuration Features: - Desktop sidebar is permanent and collapsible - Mobile sidebar opens as overlay/drawer - Active route highlighting in navigation - User profile display with email and role - Logout button in sidebar footer - Conditional Admin link (only for admin users) - Settings page showing account information Files created: - src/components/ui/sidebar.tsx (shadcn sidebar primitives) - src/components/app-sidebar.tsx (main sidebar component) - src/app/dashboard/layout.tsx (layout wrapper) - src/app/dashboard/settings/page.tsx (settings page) - components.json (shadcn configuration) Files modified: - tailwind.config.ts (added sidebar color scheme) - src/app/globals.css (added sidebar CSS variables) - src/app/dashboard/page.tsx (refactored to work with layout) Co-Authored-By: Claude Haiku 4.5 --- frontend/components.json | 11 + frontend/src/app/dashboard/layout.tsx | 27 + frontend/src/app/dashboard/page.tsx | 212 +++----- frontend/src/app/dashboard/settings/page.tsx | 76 +++ frontend/src/app/globals.css | 16 + frontend/src/components/app-sidebar.tsx | 129 +++++ frontend/src/components/ui/sidebar.tsx | 488 +++++++++++++++++++ frontend/tailwind.config.ts | 10 + 8 files changed, 838 insertions(+), 131 deletions(-) create mode 100644 frontend/components.json create mode 100644 frontend/src/app/dashboard/layout.tsx create mode 100644 frontend/src/app/dashboard/settings/page.tsx create mode 100644 frontend/src/components/app-sidebar.tsx create mode 100644 frontend/src/components/ui/sidebar.tsx diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..ec9d463 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "baseColor": "slate", + "aliases": { + "components": "./src/components", + "utils": "./src/lib/utils" + } +} diff --git a/frontend/src/app/dashboard/layout.tsx b/frontend/src/app/dashboard/layout.tsx new file mode 100644 index 0000000..58e1da6 --- /dev/null +++ b/frontend/src/app/dashboard/layout.tsx @@ -0,0 +1,27 @@ +'use client' + +import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar' +import { AppSidebar } from '@/components/app-sidebar' +import { Separator } from '@/components/ui/separator' + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + +
+ + +

Dashboard

+
+
+ {children} +
+
+
+ ) +} diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 6c34461..314b11a 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -19,12 +19,8 @@ import { Play, CheckCircle, AlertCircle, - LogOut, - Shield, Container as ContainerIcon, } from "lucide-react"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import Link from "next/link"; export default function DashboardPage() { const router = useRouter(); @@ -111,11 +107,6 @@ export default function DashboardPage() { } }; - const handleLogout = async () => { - await logout(); - router.push("/login"); - }; - if (authLoading || !user) { return (
@@ -125,136 +116,95 @@ export default function DashboardPage() { } return ( -
- {/* Header */} -
-
-
- - Container Spawner -
-
- {/* Admin-Link */} - {user.is_admin && ( - - - - )} -
- - - {user.email.slice(0, 2).toUpperCase()} - - - {user.email} - {user.is_admin && ( - - Admin - - )} -
- -
+ <> +
+

Deine Container

+

+ Verwalte deine Development- und Production-Container +

+
+ + {error && ( +
+ {error}
-
+ )} - {/* Main Content */} -
-
-

Dashboard

-

- Verwalte deine Development- und Production-Container -

+ {loading ? ( +
+
- - {error && ( -
- {error} -
- )} - - {loading ? ( -
- -
- ) : ( -
- {containers.map((container) => ( - - -
-
- - - {container.display_name} - - {container.description} -
- {getStatusIcon(container.status)} + ) : ( +
+ {containers.map((container) => ( + + +
+
+ + + {container.display_name} + + {container.description}
- - -
+ {getStatusIcon(container.status)} +
+ + +
+
+

Status:

+

{getStatusText(container.status)}

+
+ + {container.last_used && (
-

Status:

-

{getStatusText(container.status)}

+

Zuletzt verwendet:

+

+ {new Date(container.last_used).toLocaleString("de-DE")} +

+ )} - {container.last_used && ( -
-

Zuletzt verwendet:

-

- {new Date(container.last_used).toLocaleString("de-DE")} -

-
+
+ {container.status === "running" ? ( + + ) : ( + )} - -
- {container.status === "running" ? ( - - ) : ( - - )} -
- - - ))} -
- )} -
-
+
+ + + ))} + + )} + ); } diff --git a/frontend/src/app/dashboard/settings/page.tsx b/frontend/src/app/dashboard/settings/page.tsx new file mode 100644 index 0000000..05c9d42 --- /dev/null +++ b/frontend/src/app/dashboard/settings/page.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useAuth } from '@/hooks/use-auth' +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' + +export default function SettingsPage() { + const { user } = useAuth() + + return ( + <> +
+

Einstellungen

+

+ Verwalte deine Account-Einstellungen +

+
+ +
+ + + Profil + Deine Account-Informationen + + +
+ Email +

{user?.email}

+
+
+ Slug +

{user?.slug}

+
+
+ Status + + {user?.state === 'verified' ? 'Verifiziert' : user?.state === 'active' ? 'Aktiv' : 'Registriert'} + +
+ {user?.is_admin && ( +
+ Rolle + Administrator +
+ )} +
+
+ + + + Account-Info + Zusätzliche Informationen + + + {user?.created_at && ( +
+ Erstellt am +

+ {new Date(user.created_at).toLocaleString('de-DE')} +

+
+ )} + {user?.last_used && ( +
+ Zuletzt verwendet +

+ {new Date(user.last_used).toLocaleString('de-DE')} +

+
+ )} +
+
+
+ + ) +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 10c2d37..bdba020 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -23,6 +23,14 @@ --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; + --sidebar: 0 0% 100%; + --sidebar-foreground: 222.2 84% 4.9%; + --sidebar-primary: 222.2 47.4% 11.2%; + --sidebar-primary-foreground: 210 40% 98%; + --sidebar-accent: 210 40% 96.1%; + --sidebar-accent-foreground: 222.2 47.4% 11.2%; + --sidebar-border: 214.3 31.8% 91.4%; + --sidebar-ring: 222.2 84% 4.9%; --radius: 0.5rem; } @@ -46,6 +54,14 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; + --sidebar: 217.2 32.6% 17.5%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 210 40% 98%; + --sidebar-primary-foreground: 222.2 47.4% 11.2%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 212.7 26.8% 83.9%; } } diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx new file mode 100644 index 0000000..bed4b87 --- /dev/null +++ b/frontend/src/components/app-sidebar.tsx @@ -0,0 +1,129 @@ +'use client' + +import { Home, Settings, Shield, LogOut } from 'lucide-react' +import { useAuth } from '@/hooks/use-auth' +import { useRouter, usePathname } from 'next/navigation' +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from '@/components/ui/sidebar' +import { Avatar, AvatarFallback } from '@/components/ui/avatar' +import { Separator } from '@/components/ui/separator' +import Link from 'next/link' + +export function AppSidebar() { + const { user, logout } = useAuth() + const router = useRouter() + const pathname = usePathname() + + const navItems = [ + { title: 'Dashboard', url: '/dashboard', icon: Home }, + { title: 'Einstellungen', url: '/dashboard/settings', icon: Settings }, + ] + + const adminItems = user?.is_admin ? [ + { title: 'Admin', url: '/admin', icon: Shield } + ] : [] + + const handleLogout = async () => { + await logout() + router.push('/login') + } + + return ( + + +
+

Container Spawner

+
+
+ + + + + + Navigation + + + {navItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + {adminItems.length > 0 && ( + <> + + + Administration + + + {adminItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + )} + + + + + +
+ + + {user?.email?.charAt(0).toUpperCase() || 'U'} + + +
+ + {user?.email} + + + {user?.is_admin ? 'Administrator' : 'Benutzer'} + +
+
+
+ + + + Abmelden + + +
+
+
+ ) +} diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..bd285fe --- /dev/null +++ b/frontend/src/components/ui/sidebar.tsx @@ -0,0 +1,488 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const SIDEBAR_COOKIE_NAME = "sidebar:state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContextType = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(undefined) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (open: boolean) => { + setOpenProp?.(open) + _setOpen(open) + + document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp] + ) + + const state = open ? "expanded" : "collapsed" + + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile(!openMobile) : setOpen(!open) + }, [isMobile, open, setOpen, openMobile]) + + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + return ( + +
+ {children} +
+
+ ) + } +) +SidebarProvider.displayName = "SidebarProvider" + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ ) + } + + if (isMobile) { + return ( + + +
+ + + ) + } + + return ( +