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 ( + + +
+ +

{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 ( + + + + Cloud_Clipboard + + + + + + + + {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