diff --git a/eslint.config.mjs b/eslint.config.mjs
index 05e726d..18f4974 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -11,6 +11,8 @@ const eslintConfig = defineConfig([
".next/**",
"out/**",
"build/**",
+ "src/components/ui/**",
+ "src/generated/prisma/**",
"next-env.d.ts",
]),
]);
diff --git a/package-lock.json b/package-lock.json
index bda6151..0fe8541 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,9 +10,12 @@
"dependencies": {
"@prisma/client": "^6.19.0",
"@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
- "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/ssr": "^0.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -2252,6 +2255,83 @@
}
}
},
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@@ -2865,6 +2945,29 @@
}
}
},
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
+ "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
@@ -2883,6 +2986,81 @@
}
}
},
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@@ -3019,6 +3197,70 @@
}
}
},
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
diff --git a/package.json b/package.json
index c32ea54..3bdad79 100644
--- a/package.json
+++ b/package.json
@@ -15,9 +15,12 @@
"dependencies": {
"@prisma/client": "^6.19.0",
"@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
- "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/ssr": "^0.7.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/src/app/(main)/courses/page.tsx b/src/app/(main)/courses/page.tsx
new file mode 100644
index 0000000..ad5d49c
--- /dev/null
+++ b/src/app/(main)/courses/page.tsx
@@ -0,0 +1,5 @@
+const Course = () => {
+ return
Course
;
+};
+
+export default Course;
diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx
index ebaea4e..91d3369 100644
--- a/src/app/(main)/dashboard/page.tsx
+++ b/src/app/(main)/dashboard/page.tsx
@@ -1,141 +1,5 @@
-import { LogoutButton } from "@/src/components/layout/LogoutButton";
-import { requireAuthWithData, isManager, isUser } from "@/src/lib/auth/getUser";
+import DashboardView from "@/src/modules/dashboard/ui/views/dashboard-view";
-export default async function DashboardPage() {
- const userData = await requireAuthWithData();
-
- // Get display name based on user type
- const displayName = isManager(userData)
- ? `${userData.firstName} ${userData.lastName}`
- : userData.name || userData.email;
-
- return (
-
-
-
Willkommen zurück!
-
Schön, dich wiederzusehen, {displayName}
- {isManager(userData) && (
-
- Manager
-
- )}
-
-
-
-
-
Profil
-
Verwalte deine persönlichen Daten
-
-
- {isManager(userData) ? (
-
-
Meine Businesses
-
- {userData.businesses.length}{" "}
- {userData.businesses.length === 1 ? "Business" : "Businesses"}
-
-
- ) : (
-
-
Trainings
-
Deine gebuchten Trainingseinheiten
-
- )}
-
-
-
Einstellungen
-
Passe deine Einstellungen an
-
-
-
- {isUser(userData) && (
-
-
Benutzerdaten
-
-
-
- Name
- - {userData.name || "—"}
-
-
-
- E-Mail
- - {userData.email}
-
-
-
- Geburtsdatum
- -
- {userData.birthday ? new Date(userData.birthday).toLocaleDateString("de-DE") : "—"}
-
-
-
-
- Wohnort
- -
- {userData.plz && userData.city ? `${userData.plz} ${userData.city}` : "—"}
-
-
-
-
- Geschlecht
- - {userData.gender || "—"}
-
-
-
- Registriert seit
- -
- {userData.createdAt
- ? new Date(userData.createdAt).toLocaleDateString("de-DE")
- : "—"}
-
-
-
-
- )}
-
- {isManager(userData) && (
-
-
Manager-Daten
-
-
-
- Vorname
- - {userData.firstName}
-
-
-
- Nachname
- - {userData.lastName}
-
-
-
- E-Mail
- - {userData.email}
-
-
-
- Telefon
- - {userData.tel || "—"}
-
-
-
- Registriert seit
- -
- {userData.createdAt
- ? new Date(userData.createdAt).toLocaleDateString("de-DE")
- : "—"}
-
-
-
-
- )}
-
- {isManager(userData) && userData.businesses.length > 0 && (
-
-
Meine Businesses
-
- {userData.businesses.map((business) => (
-
-
{business.name}
-
{business.address}
-
{business.email}
-
- ))}
-
-
- )}
-
-
-
- );
+export default function DashboardPage() {
+ return ;
}
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
new file mode 100644
index 0000000..d60c0e6
--- /dev/null
+++ b/src/app/(main)/layout.tsx
@@ -0,0 +1,21 @@
+import { SidebarProvider } from "@/src/components/ui/sidebar";
+import DashboardNavbar from "@/src/modules/dashboard/ui/components/DashboardNavbar";
+import DashboardSidebarWrapper from "@/src/modules/dashboard/ui/components/DashboardSidebarWrapper";
+
+interface Props {
+ children: React.ReactNode;
+}
+
+const Layout = ({ children }: Props) => {
+ return (
+
+
+
+
+ {children}
+
+
+ );
+};
+
+export default Layout;
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 0bff7f2..f363387 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -1,9 +1,9 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
-
import { cn } from "@/src/lib/utils"
+
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index 15f60c4..7d711ac 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -1,6 +1,5 @@
-import * as React from "react"
-
import { cn } from "@/src/lib/utils"
+import * as React from "react"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000..12a3aae
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/src/lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..34458be
--- /dev/null
+++ b/src/components/ui/sheet.tsx
@@ -0,0 +1,139 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/src/lib/utils"
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return
+}
+
+function SheetTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ ...props
+}: React.ComponentProps & {
+ side?: "top" | "right" | "bottom" | "left"
+}) {
+ return (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+ )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
new file mode 100644
index 0000000..fe4536f
--- /dev/null
+++ b/src/components/ui/sidebar.tsx
@@ -0,0 +1,726 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+import { PanelLeftIcon } from "lucide-react"
+
+import { useIsMobile } from "@/src/hooks/use-mobile"
+import { cn } from "@/src/lib/utils"
+import { Button } from "@/src/components/ui/button"
+import { Input } from "@/src/components/ui/input"
+import { Separator } from "@/src/components/ui/separator"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/src/components/ui/sheet"
+import { Skeleton } from "@/src/components/ui/skeleton"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/src/components/ui/tooltip"
+
+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 SidebarContextProps = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+function SidebarProvider({
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}) {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ 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])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function Sidebar({
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+}
+
+function SidebarTrigger({
+ className,
+ onClick,
+ ...props
+}: React.ComponentProps) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+ return (
+
+ )
+}
+
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+} & VariantProps) {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+}
+
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+}) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showIcon?: boolean
+}) {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubButton({
+ asChild = false,
+ size = "md",
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+}) {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..9c583d8
--- /dev/null
+++ b/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/src/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..29b252c
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/src/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts
new file mode 100644
index 0000000..502fd32
--- /dev/null
+++ b/src/hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react";
+
+const MOBILE_BREAKPOINT = 768;
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined);
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
+
+ return !!isMobile;
+}
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
index ace90dc..048737e 100644
--- a/src/lib/prisma.ts
+++ b/src/lib/prisma.ts
@@ -6,6 +6,18 @@ export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ["query", "error", "warn"],
+ datasources: {
+ db: {
+ url: process.env.DATABASE_URL,
+ },
+ },
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
+
+// Graceful shutdown handler for development
+if (process.env.NODE_ENV !== "production") {
+ process.on("beforeExit", async () => {
+ await prisma.$disconnect();
+ });
+}
diff --git a/src/middleware.ts b/src/middleware.ts
index 58791a6..439dc96 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -29,7 +29,7 @@ export async function middleware(request: NextRequest) {
} = await supabase.auth.getUser();
// Protected routes
- const protectedRoutes = ["/dashboard", "/profile", "/trainings"];
+ const protectedRoutes = ["/dashboard", "/profile", "/trainings", "/courses"];
const isProtectedRoute = protectedRoutes.some((route) =>
request.nextUrl.pathname.startsWith(route)
);
diff --git a/src/modules/dashboard/ui/components/DashboardFooter.tsx b/src/modules/dashboard/ui/components/DashboardFooter.tsx
new file mode 100644
index 0000000..9a38171
--- /dev/null
+++ b/src/modules/dashboard/ui/components/DashboardFooter.tsx
@@ -0,0 +1,41 @@
+"use client";
+import { HelpCircle, Settings } from "lucide-react";
+import Link from "next/link";
+
+const footerMenu = [
+ {
+ icon: Settings,
+ label: "Einstellungen",
+ href: "/settings",
+ },
+ {
+ icon: HelpCircle,
+ label: "Hilfe",
+ href: "/help",
+ },
+];
+
+interface DashboardFooterProps {
+ displayName: string;
+}
+
+const DashboardFooter = ({ displayName }: DashboardFooterProps) => {
+ return (
+
+ {footerMenu.map((item) => (
+
+
+
+ {item.label}
+
+
+ ))}
+
+
+ );
+};
+
+export default DashboardFooter;
diff --git a/src/modules/dashboard/ui/components/DashboardNavbar.tsx b/src/modules/dashboard/ui/components/DashboardNavbar.tsx
new file mode 100644
index 0000000..215e216
--- /dev/null
+++ b/src/modules/dashboard/ui/components/DashboardNavbar.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { Button } from "@/src/components/ui/button";
+import { useSidebar } from "@/src/components/ui/sidebar";
+import { PanelLeftCloseIcon, PanelLeftIcon } from "lucide-react";
+import { usePathname } from "next/navigation";
+
+const menuTitles: Record = {
+ "/dashboard": "Dashboard",
+ "/courses": "Kurse",
+};
+
+const DashboardNavbar = () => {
+ const { state, toggleSidebar, isMobile } = useSidebar();
+ const pathname = usePathname();
+
+ const pageTitle = menuTitles[pathname] || "Dashboard";
+
+ return (
+
+
+
+
+
+
+
{pageTitle}
+
+
+
+ );
+};
+
+export default DashboardNavbar;
diff --git a/src/modules/dashboard/ui/components/DashboardSidebar.tsx b/src/modules/dashboard/ui/components/DashboardSidebar.tsx
new file mode 100644
index 0000000..ba94918
--- /dev/null
+++ b/src/modules/dashboard/ui/components/DashboardSidebar.tsx
@@ -0,0 +1,139 @@
+"use client";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/src/components/ui/sidebar";
+import {
+ ChartNoAxesCombined,
+ CreditCard,
+ DoorOpen,
+ Dumbbell,
+ IdCardLanyard,
+ LayoutDashboard,
+ MapPinned,
+ User,
+} from "lucide-react";
+
+import Image from "next/image";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import Logo from "@/src/public/logo-dark.png";
+import { cn } from "@/src/lib/utils";
+import DashboardFooter from "./DashboardFooter";
+import type { AuthUserData } from "@/src/lib/auth/getUser";
+
+const allMenuItems = [
+ {
+ icon: LayoutDashboard,
+ label: "Dashboard",
+ href: "/dashboard",
+ roles: ["manager", "user"],
+ },
+ {
+ icon: Dumbbell,
+ label: "Kurse",
+ href: "/courses",
+ roles: ["manager", "user"],
+ },
+ {
+ icon: IdCardLanyard,
+ label: "Trainer",
+ href: "/trainer",
+ roles: ["manager"],
+ },
+ {
+ icon: User,
+ label: "Kunden",
+ href: "/kunden",
+ roles: ["manager"],
+ },
+ {
+ icon: CreditCard,
+ label: "Zahlungen",
+ href: "/zahlungen",
+ roles: ["manager"],
+ },
+ {
+ icon: DoorOpen,
+ label: "Räume",
+ href: "/raume",
+ roles: ["manager"],
+ },
+ {
+ icon: ChartNoAxesCombined,
+ label: "Marketing",
+ href: "/marketing",
+ roles: ["manager"],
+ },
+ {
+ icon: MapPinned,
+ label: "Standorte",
+ href: "/standorte",
+ roles: ["manager"],
+ },
+];
+
+interface DashboardSidebarProps {
+ userData: AuthUserData;
+ displayName: string;
+}
+
+const DashboardSidebar = ({ userData, displayName }: DashboardSidebarProps) => {
+ const pathname = usePathname();
+ const userRole = userData.role;
+
+ // Filter menu items based on user role
+ const menu = allMenuItems.filter((item) => item.roles.includes(userRole));
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {menu.map((item) => (
+
+
+
+
+
+ {item.label}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DashboardSidebar;
diff --git a/src/modules/dashboard/ui/components/DashboardSidebarWrapper.tsx b/src/modules/dashboard/ui/components/DashboardSidebarWrapper.tsx
new file mode 100644
index 0000000..549dedb
--- /dev/null
+++ b/src/modules/dashboard/ui/components/DashboardSidebarWrapper.tsx
@@ -0,0 +1,13 @@
+import { requireAuthWithData, isManager } from "@/src/lib/auth/getUser";
+import DashboardSidebar from "./DashboardSidebar";
+
+const DashboardSidebarWrapper = async () => {
+ const userData = await requireAuthWithData();
+ const displayName = isManager(userData)
+ ? `${userData.firstName} ${userData.lastName}`
+ : userData.name || userData.email;
+
+ return ;
+};
+
+export default DashboardSidebarWrapper;
diff --git a/src/modules/dashboard/ui/views/dashboard-view.tsx b/src/modules/dashboard/ui/views/dashboard-view.tsx
new file mode 100644
index 0000000..6a3e9fb
--- /dev/null
+++ b/src/modules/dashboard/ui/views/dashboard-view.tsx
@@ -0,0 +1,143 @@
+import { LogoutButton } from "@/src/components/layout/LogoutButton";
+import { isManager, requireAuthWithData, isUser } from "@/src/lib/auth/getUser";
+
+const DashboardView = async () => {
+ const userData = await requireAuthWithData();
+
+ // Get display name based on user type
+ const displayName = isManager(userData)
+ ? `${userData.firstName} ${userData.lastName}`
+ : userData.name || userData.email;
+
+ return (
+
+
+
Willkommen zurück!
+
Schön, dich wiederzusehen, {displayName}
+ {isManager(userData) && (
+
+ Manager
+
+ )}
+
+
+
+
+
Profil
+
Verwalte deine persönlichen Daten
+
+
+ {isManager(userData) ? (
+
+
Meine Businesses
+
+ {userData.businesses.length}{" "}
+ {userData.businesses.length === 1 ? "Business" : "Businesses"}
+
+
+ ) : (
+
+
Trainings
+
Deine gebuchten Trainingseinheiten
+
+ )}
+
+
+
Einstellungen
+
Passe deine Einstellungen an
+
+
+
+ {isUser(userData) && (
+
+
Benutzerdaten
+
+
+
- Name
+ - {userData.name || "—"}
+
+
+
- E-Mail
+ - {userData.email}
+
+
+
- Geburtsdatum
+ -
+ {userData.birthday ? new Date(userData.birthday).toLocaleDateString("de-DE") : "—"}
+
+
+
+
- Wohnort
+ -
+ {userData.plz && userData.city ? `${userData.plz} ${userData.city}` : "—"}
+
+
+
+
- Geschlecht
+ - {userData.gender || "—"}
+
+
+
- Registriert seit
+ -
+ {userData.createdAt
+ ? new Date(userData.createdAt).toLocaleDateString("de-DE")
+ : "—"}
+
+
+
+
+ )}
+
+ {isManager(userData) && (
+
+
Manager-Daten
+
+
+
- Vorname
+ - {userData.firstName}
+
+
+
- Nachname
+ - {userData.lastName}
+
+
+
- E-Mail
+ - {userData.email}
+
+
+
- Telefon
+ - {userData.tel || "—"}
+
+
+
- Registriert seit
+ -
+ {userData.createdAt
+ ? new Date(userData.createdAt).toLocaleDateString("de-DE")
+ : "—"}
+
+
+
+
+ )}
+
+ {isManager(userData) && userData.businesses.length > 0 && (
+
+
Meine Businesses
+
+ {userData.businesses.map((business) => (
+
+
{business.name}
+
{business.address}
+
{business.email}
+
+ ))}
+
+
+ )}
+
+
+
+ );
+};
+
+export default DashboardView;
diff --git a/src/public/logo-dark.png b/src/public/logo-dark.png
new file mode 100644
index 0000000..8ac3c86
Binary files /dev/null and b/src/public/logo-dark.png differ