From 8718700fd88d8250497692375e7b74275a4d53ba Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Mon, 4 May 2026 23:33:02 +0200 Subject: [PATCH 1/4] added settings to make a publicly available site with all courses --- package-lock.json | 1671 +++++++++++++++++ package.json | 3 + prisma/schema.prisma | 7 + src/app/(main)/settings/page.tsx | 5 + src/app/business/[slug]/page.tsx | 45 + src/components/ui/calendar.tsx | 220 +++ src/components/ui/switch.tsx | 35 + src/lib/slug.ts | 15 + src/middleware.ts | 10 +- .../courses/actions/booking-actions.ts | 53 + src/modules/courses/actions/course-actions.ts | 77 + src/modules/courses/schemas/course-schema.ts | 2 + src/modules/courses/types/course.types.ts | 1 + .../ui/components/PublicCourseCard.tsx | 236 +++ .../courses/ui/views/business-public-view.tsx | 197 ++ .../courses/ui/views/course-create-view.tsx | 28 +- .../ui/components/DashboardNavbar.tsx | 1 + .../ui/components/BusinessPublicToggle.tsx | 94 + .../settings/ui/views/settings-view.tsx | 58 + 19 files changed, 2756 insertions(+), 2 deletions(-) create mode 100644 src/app/(main)/settings/page.tsx create mode 100644 src/app/business/[slug]/page.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/lib/slug.ts create mode 100644 src/modules/courses/ui/components/PublicCourseCard.tsx create mode 100644 src/modules/courses/ui/views/business-public-view.tsx create mode 100644 src/modules/settings/ui/components/BusinessPublicToggle.tsx create mode 100644 src/modules/settings/ui/views/settings-view.tsx diff --git a/package-lock.json b/package-lock.json index 15adab2..980a366 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,13 +27,16 @@ "@tiptap/starter-kit": "^3.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "dotenv": "^17.2.3", "lucide-react": "^0.552.0", "next": "16.1.6", "next-themes": "^0.4.6", "nodemailer": "^8.0.4", "prisma": "^6.19.0", + "radix-ui": "^1.4.3", "react": "19.2.0", + "react-day-picker": "^9.14.0", "react-dom": "19.2.0", "react-icons": "^5.5.0", "react-phone-number-input": "^3.4.14", @@ -1331,6 +1334,12 @@ "node": ">=18" } }, + "node_modules/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", @@ -2989,6 +2998,29 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@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-accordion": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", @@ -3061,6 +3093,75 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "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-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@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-alert-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-alert-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-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -3125,6 +3226,70 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "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-aspect-ratio/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-aspect-ratio/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-avatar": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", @@ -3406,6 +3571,75 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-context-menu/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-context-menu/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-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -3717,6 +3951,170 @@ } } }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "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-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@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-form/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "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-form/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-form/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-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "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-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-use-controllable-state": "1.2.2" + }, + "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-hover-card/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-hover-card/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-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -3839,6 +4237,302 @@ } } }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-menubar/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-menubar/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-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@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-navigation-menu/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-navigation-menu/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-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-one-time-password-field/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-one-time-password-field/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-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "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-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "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-password-toggle-field/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-password-toggle-field/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-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", @@ -4102,6 +4796,144 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@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-progress/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-progress/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-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "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-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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-radio-group/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-radio-group/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-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -4174,6 +5006,162 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-scroll-area/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-scroll-area/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-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@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-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "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-select/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-select/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-separator": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", @@ -4289,6 +5277,451 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "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-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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-switch/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-switch/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-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-tabs/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-tabs/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-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@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-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@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-toast/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-toast/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-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-toggle-group/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-toggle-group/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-toggle/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-toggle/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-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "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-toolbar/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-toolbar/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "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-toolbar/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-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -5605,6 +7038,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@tabby_ai/hijri-converter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", + "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", @@ -8334,6 +9776,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -12399,6 +13857,197 @@ ], "license": "MIT" }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@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-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@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/node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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/node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "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/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/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "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/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/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -12418,6 +14067,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", + "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "@tabby_ai/hijri-converter": "1.0.5", + "date-fns": "^4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", diff --git a/package.json b/package.json index c5849ef..2b57093 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,16 @@ "@tiptap/starter-kit": "^3.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "dotenv": "^17.2.3", "lucide-react": "^0.552.0", "next": "16.1.6", "next-themes": "^0.4.6", "nodemailer": "^8.0.4", "prisma": "^6.19.0", + "radix-ui": "^1.4.3", "react": "19.2.0", + "react-day-picker": "^9.14.0", "react-dom": "19.2.0", "react-icons": "^5.5.0", "react-phone-number-input": "^3.4.14", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b3de529..6c37e61 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -97,11 +97,15 @@ model Business { banking String? plan String? subscription Json? + slug String? @unique + isPublic Boolean @default(false) manager Manager @relation(fields: [managerId], references: [id]) + courses Course[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([managerId]) + @@index([slug]) } model Course { @@ -122,6 +126,8 @@ model Course { weekdays String[] // For custom frequency status String @default("draft") // draft, published createdBy String // Manager ID who created the course + businessId String? + business Business? @relation(fields: [businessId], references: [id], onDelete: SetNull) bookings CourseBooking[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -129,6 +135,7 @@ model Course { @@index([createdBy]) @@index([date]) @@index([status]) + @@index([businessId]) } model Room { diff --git a/src/app/(main)/settings/page.tsx b/src/app/(main)/settings/page.tsx new file mode 100644 index 0000000..a353ab8 --- /dev/null +++ b/src/app/(main)/settings/page.tsx @@ -0,0 +1,5 @@ +import SettingsView from "@/src/modules/settings/ui/views/settings-view"; + +export default function SettingsPage() { + return ; +} diff --git a/src/app/business/[slug]/page.tsx b/src/app/business/[slug]/page.tsx new file mode 100644 index 0000000..4bf5017 --- /dev/null +++ b/src/app/business/[slug]/page.tsx @@ -0,0 +1,45 @@ +import { notFound } from "next/navigation"; +import { + getBusinessBySlug, + getPublishedCoursesForBusiness, +} from "@/src/modules/courses/actions/booking-actions"; +import BusinessPublicView from "@/src/modules/courses/ui/views/business-public-view"; + +interface BusinessPageProps { + params: Promise<{ slug: string }>; +} + +export default async function BusinessPage({ params }: BusinessPageProps) { + const { slug } = await params; + + const businessResult = await getBusinessBySlug(slug); + + if (!businessResult.success || !businessResult.business || !businessResult.business.slug) { + notFound(); + } + + const business = { + ...businessResult.business, + slug: businessResult.business.slug, + }; + + const from = new Date(); + from.setHours(0, 0, 0, 0); + const to = new Date(from); + to.setDate(to.getDate() + 30); + + const coursesResult = await getPublishedCoursesForBusiness(businessResult.business.id, { + from, + to, + }); + + return ( + [0]["initialCourses"] + } + initialWindow={{ from, to }} + /> + ); +} diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..fd35809 --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,220 @@ +"use client" + +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { + DayPicker, + getDefaultClassNames, + type DayButton, +} from "react-day-picker" + +import { cn } from "@/src/lib/utils" +import { Button, buttonVariants } from "@/src/components/ui/button" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "relative flex flex-col gap-4 md:flex-row", + defaultClassNames.months + ), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), + nav: cn( + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", + defaultClassNames.button_next + ), + month_caption: cn( + "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute inset-0 bg-popover opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "font-medium select-none", + captionLayout === "label" + ? "text-sm" + : "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none", + defaultClassNames.weekday + ), + week: cn("mt-2 flex w-full", defaultClassNames.week), + week_number_header: cn( + "w-(--cell-size) select-none", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] text-muted-foreground select-none", + defaultClassNames.week_number + ), + day: cn( + "group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" + : "[&:first-child[data-selected=true]_button]:rounded-l-md", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } + + if (orientation === "right") { + return ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( + + ) : isBooked ? ( + + ) : ( + + )} +
+ + + ); +}; + +const PublicCourseCard = ({ course }: PublicCourseCardProps) => { + const [dialogOpen, setDialogOpen] = useState(false); + + const isFull = course.currentParticipants >= course.maxParticipants; + const currentLevel = levelConfig[course.level] ?? levelConfig.any; + + const formattedDate = new Date(course.date).toLocaleDateString("de-DE", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + + const formattedPrice = course.price.toLocaleString("de-DE", { + style: "currency", + currency: "EUR", + }); + + const fillPercent = + course.maxParticipants > 0 ? (course.currentParticipants / course.maxParticipants) * 100 : 0; + + return ( + <> +
+
+

{formattedDate}

+

+ {course.timeFrom} – {course.timeTo} +

+
+ +

{course.name}

+ +

{course.sport.join(", ")}

+ +
+ + {currentLevel.label} + + {formattedPrice} +
+ +
+ + {course.currentParticipants}/{course.maxParticipants} Teilnehmer + +
+ +
+
+
+ + {isFull ? ( + + ) : ( + + )} +
+ + + + ); +}; + +export default PublicCourseCard; diff --git a/src/modules/courses/ui/views/business-public-view.tsx b/src/modules/courses/ui/views/business-public-view.tsx new file mode 100644 index 0000000..ef49368 --- /dev/null +++ b/src/modules/courses/ui/views/business-public-view.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { CalendarIcon, X } from "lucide-react"; +import PublicCourseCard from "../components/PublicCourseCard"; +import { getPublishedCoursesForBusiness } from "../../actions/booking-actions"; +import { Calendar } from "@/src/components/ui/calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/src/components/ui/popover"; +import { Button } from "@/src/components/ui/button"; + +interface Business { + id: string; + name: string; + address: string; + email: string; + title: string; + slug: string; +} + +interface PublicCourse { + id: string; + name: string; + sport: string[]; + level: string; + date: Date; + timeFrom: string; + timeTo: string; + maxParticipants: number; + price: number; + currentParticipants: number; + description?: string; + room?: string; +} + +interface BusinessPublicViewProps { + business: Business; + initialCourses: PublicCourse[]; + initialWindow: { from: Date; to: Date }; +} + +const BusinessPublicView = ({ + business, + initialCourses, + initialWindow, +}: BusinessPublicViewProps) => { + const [loadedCourses, setLoadedCourses] = useState(initialCourses); + const [loadedWindow, setLoadedWindow] = useState(initialWindow); + const [selectedDate, setSelectedDate] = useState(undefined); + const [calendarOpen, setCalendarOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + + const handleDateSelect = (date: Date | undefined) => { + setCalendarOpen(false); + setSelectedDate(date); + + if (!date) return; + + const dayStart = new Date(date); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(date); + dayEnd.setHours(23, 59, 59, 999); + + // If within loaded window — filter client-side, no fetch needed + if (dayStart >= loadedWindow.from && dayEnd <= loadedWindow.to) { + return; + } + + // Outside window — fetch ±15 day window around chosen date + const newFrom = new Date(date); + newFrom.setDate(newFrom.getDate() - 15); + newFrom.setHours(0, 0, 0, 0); + const newTo = new Date(date); + newTo.setDate(newTo.getDate() + 15); + newTo.setHours(23, 59, 59, 999); + + startTransition(async () => { + const result = await getPublishedCoursesForBusiness(business.id, { + from: newFrom, + to: newTo, + }); + if (result.success && result.courses) { + setLoadedCourses(result.courses as PublicCourse[]); + setLoadedWindow({ from: newFrom, to: newTo }); + } + }); + }; + + const handleReset = () => { + setSelectedDate(undefined); + if ( + loadedWindow.from.getTime() !== initialWindow.from.getTime() || + loadedWindow.to.getTime() !== initialWindow.to.getTime() + ) { + startTransition(async () => { + const result = await getPublishedCoursesForBusiness(business.id, initialWindow); + if (result.success && result.courses) { + setLoadedCourses(result.courses as PublicCourse[]); + setLoadedWindow(initialWindow); + } + }); + } + }; + + const displayedCourses = selectedDate + ? loadedCourses.filter((c) => { + const d = new Date(c.date); + const dayStart = new Date(selectedDate); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(selectedDate); + dayEnd.setHours(23, 59, 59, 999); + return d >= dayStart && d <= dayEnd; + }) + : loadedCourses; + + const formattedSelectedDate = selectedDate + ? selectedDate.toLocaleDateString("de-DE", { weekday: "long", day: "numeric", month: "long" }) + : null; + + return ( +
+ {/* Header */} +
+
+

{business.name}

+ {business.title &&

{business.title}

} +
+ {business.address} + {business.email && ( + <> + · + + {business.email} + + + )} +
+
+
+ + {/* Courses Section */} +
+
+

Unsere Kurse

+ +
+ + + + + + + + + + {selectedDate && ( + + )} +
+
+ + {isPending ? ( +
+
+
+ ) : displayedCourses.length === 0 ? ( +
+ {selectedDate + ? "Für diesen Tag sind keine Kurse verfügbar." + : "Aktuell sind keine Kurse verfügbar."} +
+ ) : ( +
+ {displayedCourses.map((course) => ( + + ))} +
+ )} +
+
+ ); +}; + +export default BusinessPublicView; diff --git a/src/modules/courses/ui/views/course-create-view.tsx b/src/modules/courses/ui/views/course-create-view.tsx index 8c688cf..6dd67ec 100644 --- a/src/modules/courses/ui/views/course-create-view.tsx +++ b/src/modules/courses/ui/views/course-create-view.tsx @@ -8,7 +8,7 @@ import RichTextEditor from "@/src/components/layout/RichTextEditor"; import { Button } from "@/src/components/ui/button"; import { Checkbox } from "@/src/components/ui/checkbox"; import { Label } from "@/src/components/ui/label"; -import { createCourse, updateCourse } from "../../actions/course-actions"; +import { createCourse, updateCourse, getMyBusinesses } from "../../actions/course-actions"; import { getMyRooms } from "@/src/modules/rooms/actions/room-actions"; import { getMyTrainers } from "@/src/modules/employee/actions/employee-actions"; import { toast } from "sonner"; @@ -80,6 +80,8 @@ const CourseCreateView = ({ const [isLoadingRooms, setIsLoadingRooms] = useState(true); const [trainers, setTrainers] = useState>([]); const [isLoadingTrainers, setIsLoadingTrainers] = useState(true); + const [selectedBusinessId, setSelectedBusinessId] = useState(""); + const [businesses, setBusinesses] = useState>([]); const { deleteDialogOpen, @@ -93,6 +95,19 @@ const CourseCreateView = ({ onSuccess: () => router.push("/courses"), }); + // Load businesses from database + useEffect(() => { + const loadBusinesses = async () => { + const result = await getMyBusinesses(); + if (result.success) { + const opts = result.businesses.map((b) => ({ value: b.id, label: b.name })); + setBusinesses(opts); + if (opts.length === 1) setSelectedBusinessId(opts[0].value); + } + }; + loadBusinesses(); + }, []); + // Load rooms from database useEffect(() => { const loadRooms = async () => { @@ -160,6 +175,7 @@ const CourseCreateView = ({ setIsStandingOrder(initialData.isStandingOrder || false); setSelectedFrequency(initialData.frequency || ""); setSelectedWeekdays(initialData.weekdays || []); + setSelectedBusinessId(initialData.businessId ?? ""); } }, [mode, initialData, customSports]); @@ -244,6 +260,7 @@ const CourseCreateView = ({ isStandingOrder, frequency: selectedFrequency || undefined, weekdays: selectedWeekdays.length > 0 ? selectedWeekdays : undefined, + businessId: selectedBusinessId || undefined, }; if (mode === "edit" && !courseId) { @@ -298,6 +315,15 @@ const CourseCreateView = ({ )}
+ {businesses.length > 1 && ( + + )}
= { "/rooms/edit": "Bearbeite den Raum", "/employee": "Mitarbeiter Übersicht", "/employee/create": "Mitarbeiter erstellen", + "/settings": "Einstellungen", }; const dynamicRoutes: Array<{ diff --git a/src/modules/settings/ui/components/BusinessPublicToggle.tsx b/src/modules/settings/ui/components/BusinessPublicToggle.tsx new file mode 100644 index 0000000..e384db4 --- /dev/null +++ b/src/modules/settings/ui/components/BusinessPublicToggle.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useState } from "react"; +import { Switch } from "@/src/components/ui/switch"; +import { Label } from "@/src/components/ui/label"; +import { Link, Copy, Check } from "lucide-react"; +import { toast } from "sonner"; +import { toggleBusinessPublic } from "@/src/modules/courses/actions/course-actions"; + +interface BusinessPublicToggleProps { + businessId: string; + businessName: string; + initialIsPublic: boolean; + initialSlug: string | null; +} + +const BusinessPublicToggle = ({ + businessId, + businessName, + initialIsPublic, + initialSlug, +}: BusinessPublicToggleProps) => { + const [isPublic, setIsPublic] = useState(initialIsPublic); + const [slug, setSlug] = useState(initialSlug); + const [isLoading, setIsLoading] = useState(false); + const [copied, setCopied] = useState(false); + + const publicUrl = slug + ? `${typeof window !== "undefined" ? window.location.origin : ""}/business/${slug}` + : null; + + const handleToggle = async (checked: boolean) => { + setIsLoading(true); + const result = await toggleBusinessPublic(businessId, checked); + if (result.success) { + setIsPublic(checked); + if (result.slug) setSlug(result.slug); + toast.success(checked ? "Seite veröffentlicht" : "Seite versteckt"); + } else { + toast.error(result.error ?? "Fehler beim Aktualisieren"); + } + setIsLoading(false); + }; + + const handleCopy = () => { + if (!publicUrl) return; + navigator.clipboard.writeText(publicUrl); + setCopied(true); + toast.success("Link kopiert"); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+
+

{businessName}

+

Öffentliche Kursseite

+
+
+ + +
+
+ + {isPublic && publicUrl && ( +
+ + {publicUrl} + +
+ )} +
+ ); +}; + +export default BusinessPublicToggle; diff --git a/src/modules/settings/ui/views/settings-view.tsx b/src/modules/settings/ui/views/settings-view.tsx new file mode 100644 index 0000000..9a73222 --- /dev/null +++ b/src/modules/settings/ui/views/settings-view.tsx @@ -0,0 +1,58 @@ +import { requireAuthWithData, isManager } from "@/src/lib/auth/getUser"; +import { prisma } from "@/src/lib/prisma"; +import BusinessPublicToggle from "../components/BusinessPublicToggle"; + +const SettingsView = async () => { + const userData = await requireAuthWithData(); + + if (!isManager(userData)) { + return ( +
+

Einstellungen

+

Keine weiteren Einstellungen verfügbar.

+
+ ); + } + + const businesses = await prisma.business.findMany({ + where: { managerId: userData.id }, + select: { id: true, name: true, isPublic: true, slug: true }, + orderBy: { name: "asc" }, + }); + + // If manager has exactly one business, auto-assign any unassigned courses to it + if (businesses.length === 1) { + await prisma.course.updateMany({ + where: { createdBy: userData.id, businessId: null }, + data: { businessId: businesses[0].id }, + }); + } + + return ( +
+
+

Einstellungen

+

Verwalte die öffentlichen Seiten deiner Businesses

+
+ + {businesses.length === 0 ? ( +

Noch kein Business angelegt.

+ ) : ( +
+

Öffentliche Seiten

+ {businesses.map((business) => ( + + ))} +
+ )} +
+ ); +}; + +export default SettingsView; From 87dd836ddcc47bad1fb0eea569549f2dd4a34fb6 Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Tue, 5 May 2026 10:45:05 +0200 Subject: [PATCH 2/4] see comments --- package-lock.json | 28 +++++++++++++++++++ package.json | 2 ++ prisma/schema.prisma | 1 - src/lib/slug.ts | 5 ++-- src/modules/courses/actions/course-actions.ts | 28 +++++++++++++------ .../ui/components/PublicCourseCard.tsx | 3 +- .../courses/ui/views/course-create-view.tsx | 2 ++ .../employee/actions/employee-actions.ts | 4 +++ .../ui/components/BusinessPublicToggle.tsx | 14 ++++++---- .../settings/ui/views/settings-view.tsx | 8 ------ 10 files changed, 70 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 980a366..43ef183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "dompurify": "^3.4.2", "dotenv": "^17.2.3", "lucide-react": "^0.552.0", "next": "16.1.6", @@ -51,6 +52,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/dompurify": "^3.0.5", "@types/node": "^20", "@types/nodemailer": "^7.0.4", "@types/react": "^19", @@ -7945,6 +7947,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -8046,6 +8058,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -9927,6 +9946,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", + "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", diff --git a/package.json b/package.json index 2b57093..4383629 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "dompurify": "^3.4.2", "dotenv": "^17.2.3", "lucide-react": "^0.552.0", "next": "16.1.6", @@ -58,6 +59,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/dompurify": "^3.0.5", "@types/node": "^20", "@types/nodemailer": "^7.0.4", "@types/react": "^19", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6c37e61..fe4efea 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -105,7 +105,6 @@ model Business { updatedAt DateTime @updatedAt @@index([managerId]) - @@index([slug]) } model Course { diff --git a/src/lib/slug.ts b/src/lib/slug.ts index 0b5ae92..9c99bb5 100644 --- a/src/lib/slug.ts +++ b/src/lib/slug.ts @@ -1,12 +1,13 @@ import { PrismaClient } from "@prisma/client"; export async function generateUniqueSlug(name: string, prisma: PrismaClient): Promise { - const base = name + const normalized = name .toLowerCase() .replace(/\s+/g, "-") .replace(/[^a-z0-9-]/g, "") .slice(0, 60); - let slug = base || "business"; + const base = normalized || "business"; + let slug = base; let counter = 2; while (await prisma.business.findUnique({ where: { slug } })) { slug = `${base}-${counter++}`; diff --git a/src/modules/courses/actions/course-actions.ts b/src/modules/courses/actions/course-actions.ts index 0e05fb0..bb3b8c0 100644 --- a/src/modules/courses/actions/course-actions.ts +++ b/src/modules/courses/actions/course-actions.ts @@ -554,17 +554,29 @@ export async function toggleBusinessPublic(businessId: string, isPublic: boolean return { success: false, error: "Business nicht gefunden" }; } - // Generate slug on first publish if missing + // Generate slug on first publish if missing, retry on P2002 race condition let slug = business.slug; - if (!slug) { - slug = await generateUniqueSlug(business.name, prisma); + let updated = false; + while (!updated) { + if (!slug) { + slug = await generateUniqueSlug(business.name, prisma); + } + try { + await prisma.business.update({ + where: { id: businessId }, + data: { isPublic, slug }, + }); + updated = true; + } catch (err) { + if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") { + // Another request claimed this slug concurrently — generate a new one and retry + slug = null; + } else { + throw err; + } + } } - await prisma.business.update({ - where: { id: businessId }, - data: { isPublic, slug }, - }); - // Auto-assign courses that belong to this manager but have no businessId yet. // Only do this when there is exactly one business — if there are multiple, // the manager must assign courses manually via the course edit form. diff --git a/src/modules/courses/ui/components/PublicCourseCard.tsx b/src/modules/courses/ui/components/PublicCourseCard.tsx index 14f316a..d9bf6a7 100644 --- a/src/modules/courses/ui/components/PublicCourseCard.tsx +++ b/src/modules/courses/ui/components/PublicCourseCard.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { Button } from "@/src/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/src/components/ui/dialog"; +import DOMPurify from "dompurify"; import { bookCourse, checkUserBookingStatus } from "../../actions/booking-actions"; import { toast } from "sonner"; @@ -135,7 +136,7 @@ const BookingDialog = ({ {course.description && (
)}
diff --git a/src/modules/courses/ui/views/course-create-view.tsx b/src/modules/courses/ui/views/course-create-view.tsx index 6dd67ec..441afcb 100644 --- a/src/modules/courses/ui/views/course-create-view.tsx +++ b/src/modules/courses/ui/views/course-create-view.tsx @@ -103,6 +103,8 @@ const CourseCreateView = ({ const opts = result.businesses.map((b) => ({ value: b.id, label: b.name })); setBusinesses(opts); if (opts.length === 1) setSelectedBusinessId(opts[0].value); + } else { + toast.error("Fehler beim Laden der Standorte. Bitte laden Sie die Seite neu."); } }; loadBusinesses(); diff --git a/src/modules/employee/actions/employee-actions.ts b/src/modules/employee/actions/employee-actions.ts index 6a6a7c6..7f2400e 100644 --- a/src/modules/employee/actions/employee-actions.ts +++ b/src/modules/employee/actions/employee-actions.ts @@ -518,6 +518,10 @@ export async function updateEmployee( revalidatePath("/employee"); revalidatePath(`/employee/edit/${employeeId}`); invalidateUserDataCache(validatedData.email); + // If email changed, also invalidate the old email's cache entry + if (existingEmployee.email !== validatedData.email) { + invalidateUserDataCache(existingEmployee.email); + } return { success: true, diff --git a/src/modules/settings/ui/components/BusinessPublicToggle.tsx b/src/modules/settings/ui/components/BusinessPublicToggle.tsx index e384db4..de36758 100644 --- a/src/modules/settings/ui/components/BusinessPublicToggle.tsx +++ b/src/modules/settings/ui/components/BusinessPublicToggle.tsx @@ -42,12 +42,16 @@ const BusinessPublicToggle = ({ setIsLoading(false); }; - const handleCopy = () => { + const handleCopy = async () => { if (!publicUrl) return; - navigator.clipboard.writeText(publicUrl); - setCopied(true); - toast.success("Link kopiert"); - setTimeout(() => setCopied(false), 2000); + try { + await navigator.clipboard.writeText(publicUrl); + setCopied(true); + toast.success("Link kopiert"); + setTimeout(() => setCopied(false), 2000); + } catch { + toast.error("Link konnte nicht kopiert werden"); + } }; return ( diff --git a/src/modules/settings/ui/views/settings-view.tsx b/src/modules/settings/ui/views/settings-view.tsx index 9a73222..54df70a 100644 --- a/src/modules/settings/ui/views/settings-view.tsx +++ b/src/modules/settings/ui/views/settings-view.tsx @@ -20,14 +20,6 @@ const SettingsView = async () => { orderBy: { name: "asc" }, }); - // If manager has exactly one business, auto-assign any unassigned courses to it - if (businesses.length === 1) { - await prisma.course.updateMany({ - where: { createdBy: userData.id, businessId: null }, - data: { businessId: businesses[0].id }, - }); - } - return (
From a17047895b467b07f48703b919750fd83c655d80 Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Tue, 5 May 2026 10:56:10 +0200 Subject: [PATCH 3/4] see comments from commit #88 --- src/lib/auth/getUser.ts | 15 ++++++- src/modules/courses/actions/course-actions.ts | 42 +++++++++++++++---- .../employee/actions/employee-actions.ts | 37 ++++++++++++---- src/modules/rooms/actions/room-actions.ts | 26 +++++++++--- 4 files changed, 98 insertions(+), 22 deletions(-) diff --git a/src/lib/auth/getUser.ts b/src/lib/auth/getUser.ts index 06a7bb5..9bd1c2e 100644 --- a/src/lib/auth/getUser.ts +++ b/src/lib/auth/getUser.ts @@ -145,7 +145,12 @@ export const getUserData = cache(async (): Promise => { let permissions: EmployeeData["permissions"]; - if (raw && ("canCreateCourses" in raw || "canCreateEmployees" in raw)) { + if ( + raw && + typeof raw === "object" && + !Array.isArray(raw) && + ("canCreateCourses" in raw || "canCreateEmployees" in raw) + ) { // Backward-Kompatibilität: altes Format migrieren const old = raw as { canCreateCourses?: boolean; canCreateEmployees?: boolean }; permissions = { @@ -210,6 +215,14 @@ export const getUserData = cache(async (): Promise => { )(user.email, user.id); }); +// Returns the effective manager ID for any user type. +// For Employees, this is createdBy (the manager who created them). +// Returns null if the employee is orphaned (no manager mapping) — callers must handle this. +export function getEffectiveManagerId(userData: AuthUserData): string | null { + if (isEmployee(userData)) return userData.createdBy; + return userData.id; +} + export async function requireAuth() { const user = await getUser(); return user!; diff --git a/src/modules/courses/actions/course-actions.ts b/src/modules/courses/actions/course-actions.ts index bb3b8c0..903a1e8 100644 --- a/src/modules/courses/actions/course-actions.ts +++ b/src/modules/courses/actions/course-actions.ts @@ -9,7 +9,7 @@ "use server"; import { prisma } from "@/src/lib/prisma"; -import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser"; +import { getUserData, isManager, isEmployee, getEffectiveManagerId } from "@/src/lib/auth/getUser"; import { courseSchema, publishedCourseSchema, type CourseFormData } from "../schemas/course-schema"; import { revalidatePath } from "next/cache"; import { Prisma } from "@prisma/client"; @@ -63,7 +63,9 @@ export async function createCourse(data: CourseFormData, status: "draft" | "publ const validatedData = validation.data; // Schritt 5: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - const creatorId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const creatorId = getEffectiveManagerId(userData); + if (!creatorId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 6: Kurs in der Datenbank erstellen const course = await prisma.course.create({ @@ -173,7 +175,13 @@ export async function getMyCourses(options?: { dateFrom?: Date; dateTo?: Date }) } // Step 3: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { + success: false, + error: "Mitarbeiter-Account ist keinem Manager zugeordnet", + courses: [], + }; // Step 4: Build the where clause with optional date filtering const whereClause: Prisma.CourseWhereInput = { @@ -248,7 +256,9 @@ export async function getCourseById(courseId: string) { } // Schritt 3: Effektive Manager-ID ermitteln - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 4: Kurs aus der Datenbank abrufen const course = await prisma.course.findUnique({ @@ -324,7 +334,9 @@ export async function updateCourse( } // Schritt 4: Effektive Manager-ID ermitteln - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 5: Kurs aus der Datenbank abrufen const existingCourse = await prisma.course.findUnique({ @@ -434,7 +446,9 @@ export async function deleteCourse(courseId: string) { } // Schritt 3: Effektive Manager-ID ermitteln - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 4: Kurs aus der Datenbank abrufen const course = await prisma.course.findUnique({ @@ -492,7 +506,13 @@ export async function getMySportTypes() { return { success: false, error: "Unauthorized", sports: [] }; } - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { + success: false, + error: "Mitarbeiter-Account ist keinem Manager zugeordnet", + sports: [], + }; const courses = await prisma.course.findMany({ where: { createdBy: managerId }, @@ -522,7 +542,13 @@ export async function getMyBusinesses() { return { success: false, error: "Unauthorized", businesses: [] }; } - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { + success: false, + error: "Mitarbeiter-Account ist keinem Manager zugeordnet", + businesses: [], + }; const businesses = await prisma.business.findMany({ where: { managerId }, diff --git a/src/modules/employee/actions/employee-actions.ts b/src/modules/employee/actions/employee-actions.ts index 7f2400e..ec06f5a 100644 --- a/src/modules/employee/actions/employee-actions.ts +++ b/src/modules/employee/actions/employee-actions.ts @@ -9,7 +9,7 @@ "use server"; import { prisma } from "@/src/lib/prisma"; -import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser"; +import { getUserData, isManager, isEmployee, getEffectiveManagerId } from "@/src/lib/auth/getUser"; import { createAdminClient } from "@/src/lib/supabase/admin"; import { employeeSchema, type EmployeeFormData } from "../schemas/employee-schema"; import { revalidatePath, revalidateTag } from "next/cache"; @@ -129,7 +129,9 @@ export async function createEmployee(data: EmployeeFormData, status: "draft" | " } // Schritt 7: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 8: Berechtigungen auf Caller-Ceiling begrenzen (Privilege-Escalation verhindern) const finalPermissions = isEmployee(userData) @@ -216,7 +218,14 @@ export async function getMyTrainers() { }; } - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { + success: false, + error: "Mitarbeiter-Account ist keinem Manager zugeordnet", + employees: [], + trainers: [], + }; const employees = await prisma.employee.findMany({ where: { @@ -282,8 +291,12 @@ export async function getMyEmployees() { // Manager sieht alle Mitarbeiter unter seiner ID whereClause = { createdBy: userData.id }; } else { - // Employee: eigene Manager-ID ermitteln, dann alle unter diesem Manager anzeigen (außer sich selbst) + // Employee: alle unter demselben Manager anzeigen (außer sich selbst) + // createdBy kann null sein (verwaister Account) — in dem Fall nichts zurückgeben const managerId = userData.createdBy; + if (!managerId) { + return { success: true, employees: [] }; + } whereClause = { createdBy: managerId, NOT: { email: userData.email } }; } const employees = await prisma.employee.findMany({ @@ -435,7 +448,9 @@ export async function updateEmployee( } // Schritt 6: Überprüfen, ob der Benutzer der Besitzer des Mitarbeiters ist - const ownerManagerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const ownerManagerId = getEffectiveManagerId(userData); + if (!ownerManagerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; if (existingEmployee.createdBy !== ownerManagerId) { return { success: false, @@ -568,7 +583,13 @@ export async function getMyEmployeeRoles() { } // Schritt 3: Effektive Manager-ID ermitteln - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { + success: false, + error: "Mitarbeiter-Account ist keinem Manager zugeordnet", + roles: [], + }; // Schritt 4: Alle Mitarbeiter des Managers abrufen (nur roles Feld) const employees = await prisma.employee.findMany({ @@ -653,7 +674,9 @@ export async function deleteEmployee(employeeId: string) { } // Schritt 5: Überprüfen, ob der Benutzer der Besitzer des Mitarbeiters ist - const ownerManagerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const ownerManagerId = getEffectiveManagerId(userData); + if (!ownerManagerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; if (employee.createdBy !== ownerManagerId) { return { success: false, diff --git a/src/modules/rooms/actions/room-actions.ts b/src/modules/rooms/actions/room-actions.ts index 682772c..3e9241a 100644 --- a/src/modules/rooms/actions/room-actions.ts +++ b/src/modules/rooms/actions/room-actions.ts @@ -9,7 +9,7 @@ "use server"; import { prisma } from "@/src/lib/prisma"; -import { getUserData, isManager, isEmployee } from "@/src/lib/auth/getUser"; +import { getUserData, isManager, isEmployee, getEffectiveManagerId } from "@/src/lib/auth/getUser"; import { roomSchema, type RoomFormData } from "../schemas/room-schema"; import { revalidatePath } from "next/cache"; @@ -59,7 +59,9 @@ export async function createRoom(data: RoomFormData) { const validatedData = validation.data; // Schritt 5: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - const creatorId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const creatorId = getEffectiveManagerId(userData); + if (!creatorId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 6: Raum in der Datenbank erstellen const room = await prisma.room.create({ @@ -143,7 +145,13 @@ export async function getMyRooms() { } // Schritt 3: Manager-ID ermitteln (Employee erbt createdBy vom eigenen Datensatz) - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { + success: false, + error: "Mitarbeiter-Account ist keinem Manager zugeordnet", + rooms: [], + }; // Schritt 4: Räume abrufen, die von diesem Manager erstellt wurden const rooms = await prisma.room.findMany({ @@ -195,7 +203,9 @@ export async function getRoomById(roomId: string) { } // Schritt 3: Effektive Manager-ID ermitteln - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 4: Raum aus der Datenbank abrufen const room = await prisma.room.findUnique({ @@ -267,7 +277,9 @@ export async function updateRoom(roomId: string, data: RoomFormData) { } // Schritt 4: Effektive Manager-ID ermitteln - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 5: Raum aus der Datenbank abrufen const existingRoom = await prisma.room.findUnique({ @@ -356,7 +368,9 @@ export async function deleteRoom(roomId: string) { } // Schritt 3: Effektive Manager-ID ermitteln - const managerId = isEmployee(userData) ? (userData.createdBy ?? userData.id) : userData.id; + const managerId = getEffectiveManagerId(userData); + if (!managerId) + return { success: false, error: "Mitarbeiter-Account ist keinem Manager zugeordnet" }; // Schritt 4: Raum aus der Datenbank abrufen const room = await prisma.room.findUnique({ From 060924fcb3b14e211fe0bee40871f5fdec3ae766 Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Tue, 5 May 2026 11:00:39 +0200 Subject: [PATCH 4/4] see comments ui fixes --- .../ui/components/PublicCourseCard.tsx | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/modules/courses/ui/components/PublicCourseCard.tsx b/src/modules/courses/ui/components/PublicCourseCard.tsx index d9bf6a7..e321a13 100644 --- a/src/modules/courses/ui/components/PublicCourseCard.tsx +++ b/src/modules/courses/ui/components/PublicCourseCard.tsx @@ -48,8 +48,10 @@ const BookingDialog = ({ const [isBooked, setIsBooked] = useState(false); const [isCheckingStatus, setIsCheckingStatus] = useState(true); - const isFull = course.currentParticipants >= course.maxParticipants; - const spotsLeft = course.maxParticipants - course.currentParticipants; + const safeCurrent = Math.max(0, course.currentParticipants); + const safeMax = Math.max(0, course.maxParticipants); + const isFull = safeCurrent >= safeMax; + const spotsLeft = Math.max(0, safeMax - safeCurrent); const formattedDate = new Date(course.date).toLocaleDateString("de-DE", { weekday: "long", @@ -65,11 +67,26 @@ const BookingDialog = ({ useEffect(() => { if (!open) return; - setIsCheckingStatus(true); - checkUserBookingStatus(course.id).then((result) => { - if (result.success) setIsBooked(result.isBooked); - setIsCheckingStatus(false); - }); + let active = true; + + const loadStatus = async () => { + setIsCheckingStatus(true); + try { + const result = await checkUserBookingStatus(course.id); + if (!active) return; + setIsBooked(result.success ? result.isBooked : false); + } catch { + if (!active) return; + setIsBooked(false); + } finally { + if (active) setIsCheckingStatus(false); + } + }; + + void loadStatus(); + return () => { + active = false; + }; }, [open, course.id]); const handleBooking = async () => { @@ -128,7 +145,7 @@ const BookingDialog = ({
0 ? (course.currentParticipants / course.maxParticipants) * 100 : 0}%`, + width: `${safeMax > 0 ? Math.min(100, (safeCurrent / safeMax) * 100) : 0}%`, }} />
@@ -164,7 +181,9 @@ const BookingDialog = ({ const PublicCourseCard = ({ course }: PublicCourseCardProps) => { const [dialogOpen, setDialogOpen] = useState(false); - const isFull = course.currentParticipants >= course.maxParticipants; + const cardSafeCurrent = Math.max(0, course.currentParticipants); + const cardSafeMax = Math.max(0, course.maxParticipants); + const isFull = cardSafeCurrent >= cardSafeMax; const currentLevel = levelConfig[course.level] ?? levelConfig.any; const formattedDate = new Date(course.date).toLocaleDateString("de-DE", { @@ -179,8 +198,7 @@ const PublicCourseCard = ({ course }: PublicCourseCardProps) => { currency: "EUR", }); - const fillPercent = - course.maxParticipants > 0 ? (course.currentParticipants / course.maxParticipants) * 100 : 0; + const fillPercent = cardSafeMax > 0 ? Math.min(100, (cardSafeCurrent / cardSafeMax) * 100) : 0; return ( <>