diff --git a/package-lock.json b/package-lock.json index 12c47a3..850ad21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@hookform/resolvers": "^5.0.1", "@payloadcms/admin-bar": "3.38.0", "@payloadcms/db-postgres": "3.38.0", "@payloadcms/live-preview-react": "3.38.0", @@ -27,20 +28,22 @@ "@radix-ui/react-collapsible": "^1.1.10", "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.14", - "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.6", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.6", "@tabler/icons-react": "^3.33.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cross-env": "^7.0.3", "date-fns": "^4.1.0", + "framer-motion": "^12.12.1", "geist": "^1.3.0", "graphql": "^16.8.2", "lucide-react": "^0.378.0", + "motion": "^12.12.1", "next": "15.3.0", "next-sitemap": "^4.2.3", "payload": "3.38.0", @@ -48,10 +51,12 @@ "react": "19.1.0", "react-day-picker": "^8.10.1", "react-dom": "19.1.0", - "react-hook-form": "7.45.4", + "react-hook-form": "^7.45.4", + "react-use-measure": "^2.1.7", "sharp": "0.32.6", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", + "zod": "^3.25.7", "zustand": "^5.0.4" }, "devDependencies": { @@ -2493,6 +2498,18 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@hookform/resolvers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz", + "integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -4074,6 +4091,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "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-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -4140,6 +4175,24 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -4270,12 +4323,35 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.6.tgz", - "integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==", + "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.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-label/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": "*", @@ -4332,6 +4408,24 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "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.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.13.tgz", @@ -4369,6 +4463,24 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "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-popper": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", @@ -4472,6 +4584,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "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.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", @@ -4546,6 +4676,24 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "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.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.6.tgz", @@ -4570,9 +4718,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "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" @@ -4621,6 +4769,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -5549,6 +5715,12 @@ "node": ">=18.0.0" } }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -8937,6 +9109,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.12.1.tgz", + "integrity": "sha512-PFw4/GCREHI2suK/NlPSUxd+x6Rkp80uQsfCRFSOQNrm5pZif7eGtmG1VaD/UF1fW9tRBy5AaS77StatB3OJDg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.12.1", + "motion-utils": "^12.12.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -11025,6 +11224,47 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/motion": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.12.1.tgz", + "integrity": "sha512-vN/3p++Ix0lVt9NH0ZqPrAy8QRTkff27t5z3z5+4BJ3cXPxtOia2EBZS4snM+JUTfl7J0JXP/5ERqu9GT/8IgA==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.12.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.12.1.tgz", + "integrity": "sha512-GXq/uUbZBEiFFE+K1Z/sxdPdadMdfJ/jmBALDfIuHGi0NmtealLOfH9FqT+6aNPgVx8ilq0DtYmyQlo6Uj9LKQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.12.1" + } + }, + "node_modules/motion-utils": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz", + "integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12773,6 +13013,21 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15391,6 +15646,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.7.tgz", + "integrity": "sha512-YGdT1cVRmKkOg6Sq7vY7IkxdphySKnXhaUmFI4r4FcuFVNgpCb9tZfNwXbT6BPjD5oz0nubFsoo9pIqKrDcCvg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zustand": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", diff --git a/package.json b/package.json index 6d99584..5044aa9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "start": "cross-env NODE_OPTIONS=--no-deprecation next start" }, "dependencies": { + "@hookform/resolvers": "^5.0.1", "@payloadcms/admin-bar": "3.38.0", "@payloadcms/db-postgres": "3.38.0", "@payloadcms/live-preview-react": "3.38.0", @@ -37,20 +38,22 @@ "@radix-ui/react-collapsible": "^1.1.10", "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.14", - "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.13", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.6", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.6", "@tabler/icons-react": "^3.33.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cross-env": "^7.0.3", "date-fns": "^4.1.0", + "framer-motion": "^12.12.1", "geist": "^1.3.0", "graphql": "^16.8.2", "lucide-react": "^0.378.0", + "motion": "^12.12.1", "next": "15.3.0", "next-sitemap": "^4.2.3", "payload": "3.38.0", @@ -58,10 +61,12 @@ "react": "19.1.0", "react-day-picker": "^8.10.1", "react-dom": "19.1.0", - "react-hook-form": "7.45.4", + "react-hook-form": "^7.45.4", + "react-use-measure": "^2.1.7", "sharp": "0.32.6", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", + "zod": "^3.25.7", "zustand": "^5.0.4" }, "devDependencies": { diff --git a/src/app/(frontend)/[tenant]/dashboard/workouts/layout.client.tsx b/src/app/(frontend)/[tenant]/dashboard/workouts/layout.client.tsx index b09615d..adfa3c1 100644 --- a/src/app/(frontend)/[tenant]/dashboard/workouts/layout.client.tsx +++ b/src/app/(frontend)/[tenant]/dashboard/workouts/layout.client.tsx @@ -1,6 +1,6 @@ 'use client' -import { Exercise, ExerciseType, Workout } from '@/payload-types' +import { Exercise, ExerciseType, Workout, WorkoutType } from '@/payload-types' import useWorkouts from '@/stores/Workouts' import { PaginatedDocs } from 'payload' import { ReactNode, use, useEffect } from 'react' @@ -8,6 +8,7 @@ import { ReactNode, use, useEffect } from 'react' type Props = { children: ReactNode getTenantWorkoutsPromise: Promise> + getTenantWorkoutTypesPromise: Promise> getTenantExercisesPromise: Promise> getTenantExerciseTypesPromise: Promise> } @@ -17,13 +18,15 @@ const WorkoutsLayoutSuspendedFrontend = (props: Props) => { getTenantExerciseTypesPromise, getTenantExercisesPromise, getTenantWorkoutsPromise, + getTenantWorkoutTypesPromise, } = props const exerciseTypeResponse = use(getTenantExerciseTypesPromise) const exerciseResponse = use(getTenantExercisesPromise) const workoutResponse = use(getTenantWorkoutsPromise) + const workoutTypeResponse = use(getTenantWorkoutTypesPromise) - const { setExerciseTypes, setExercises, setWorkouts } = useWorkouts() + const { setExerciseTypes, setExercises, setWorkouts, setWorkoutTypes } = useWorkouts() useEffect(() => { if (exerciseTypeResponse?.docs?.length) setExerciseTypes(exerciseTypeResponse) @@ -33,6 +36,10 @@ const WorkoutsLayoutSuspendedFrontend = (props: Props) => { if (exerciseResponse?.docs?.length) setExercises(exerciseResponse) }, [exerciseResponse]) + useEffect(() => { + if (workoutTypeResponse?.docs?.length) setWorkoutTypes(workoutTypeResponse) + }, [workoutResponse]) + useEffect(() => { if (workoutResponse?.docs?.length) setWorkouts(workoutResponse) }, [workoutResponse]) diff --git a/src/app/(frontend)/[tenant]/dashboard/workouts/layout.tsx b/src/app/(frontend)/[tenant]/dashboard/workouts/layout.tsx index e2118d0..d9bb456 100644 --- a/src/app/(frontend)/[tenant]/dashboard/workouts/layout.tsx +++ b/src/app/(frontend)/[tenant]/dashboard/workouts/layout.tsx @@ -2,7 +2,9 @@ import { ReactNode } from 'react' import configPromise from '@payload-config' import { getPayload, PaginatedDocs } from 'payload' import WorkoutsLayoutSuspendedFrontend from './layout.client' -import { Exercise, ExerciseType } from '@/payload-types' +import { Exercise, ExerciseType, Workout, WorkoutType } from '@/payload-types' + +const TYPE_LIMIT = 50 type Props = { params: Promise<{ @@ -18,7 +20,7 @@ const WorkoutsLayout = async (props: Props) => { const getExerciseTypesPromise = payload.find({ collection: 'exerciseTypes', - limit: 50, + limit: TYPE_LIMIT, depth: 0, select: { id: true, @@ -51,6 +53,22 @@ const WorkoutsLayout = async (props: Props) => { }, }) as Promise> + const getWorkoutTypesPromise = payload.find({ + collection: 'workoutTypes', + limit: TYPE_LIMIT, + depth: 0, + select: { + id: true, + name: true, + description: true, + }, + where: { + 'tenant.slug': { + equals: tenantSlug, + }, + }, + }) as Promise> + const getWorkoutsPromise = payload.find({ collection: 'workouts', limit: 20, @@ -68,12 +86,13 @@ const WorkoutsLayout = async (props: Props) => { equals: tenantSlug, }, }, - }) as Promise> + }) as Promise> return ( {children} diff --git a/src/app/(frontend)/[tenant]/dashboard/workouts/page.tsx b/src/app/(frontend)/[tenant]/dashboard/workouts/page.tsx index 967a560..4ca7837 100644 --- a/src/app/(frontend)/[tenant]/dashboard/workouts/page.tsx +++ b/src/app/(frontend)/[tenant]/dashboard/workouts/page.tsx @@ -1,13 +1,19 @@ import { DashboardContent, DashboardContentSection } from '@/components/Dashboard' +import WorkoutPagiation from '@/components/Workouts/WorkoutPagiation' +import WorkoutListView from '@/components/Workouts/WorkoutsListView' // import WorkoutsPageClient from './page.client' const WorkoutsPage = () => { return ( - -

Workouts

+ Workouts} + footing={} + > + - +

Exercises

diff --git a/src/app/(frontend)/globals.css b/src/app/(frontend)/globals.css index 9d69cc1..48f0b72 100644 --- a/src/app/(frontend)/globals.css +++ b/src/app/(frontend)/globals.css @@ -34,7 +34,7 @@ --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; + --accent: 32.1 94.6% 43.7%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; @@ -44,7 +44,7 @@ --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; - --radius: 0.2rem; + --radius: 0.3rem; --success: 196 52% 74%; --warning: 34 89% 85%; @@ -53,7 +53,7 @@ --sidebar-foreground: 240 5.3% 26.1%; --sidebar-primary: 240 5.9% 10%; --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent: 32.1 94.6% 43.7%; --sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; @@ -69,8 +69,8 @@ --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 48 96.6% 76.7%; + --primary-foreground: 240 4.8% 15.9%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; @@ -78,8 +78,8 @@ --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 48 96.6% 76.7%; + --accent-foreground: 240 4.8% 15.9%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; @@ -95,10 +95,10 @@ --sidebar-background: 240 5.9% 10%; --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-primary: 48 96.6% 76.7%; + --sidebar-primary-foreground: 240 4.8% 15.9%; + --sidebar-accent: 48 96.6% 76.7%; + --sidebar-accent-foreground: 240 4.8% 15.9%; --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; } @@ -122,8 +122,8 @@ --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 48 96.6% 76.7%; + --primary-foreground: 240 4.8% 15.9%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; diff --git a/src/collections/Workouts/index.ts b/src/collections/Workouts/index.ts index 2ba5494..288c30c 100644 --- a/src/collections/Workouts/index.ts +++ b/src/collections/Workouts/index.ts @@ -76,6 +76,29 @@ export const Workouts: CollectionConfig = { type: 'text', unique: true, }, + { + type: 'row', + fields: [ + { + name: 'avatar', + type: 'relationship', + relationTo: 'media', + hasMany: false, + admin: { + width: '50%', + }, + }, + { + name: 'displayImage', + type: 'relationship', + relationTo: 'media', + hasMany: false, + admin: { + width: '50%', + }, + }, + ], + }, { name: 'type', type: 'relationship', diff --git a/src/components/Dashboard/DashboardContentSection.tsx b/src/components/Dashboard/DashboardContentSection.tsx index 5af00b5..c32c7ac 100644 --- a/src/components/Dashboard/DashboardContentSection.tsx +++ b/src/components/Dashboard/DashboardContentSection.tsx @@ -5,13 +5,27 @@ import { ReactNode } from 'react' type Props = { children: ReactNode className?: ClassValue + heading?: ReactNode + footing?: ReactNode } const DashboardContentSection = (props: Props) => { - const { children, className } = props + const { children, className, heading, footing } = props return ( -
- {children} +
+ {!!heading && ( +
+ {heading} +
+ )} + +
{children}
+ + {!!footing && ( +
+ {footing} +
+ )}
) } diff --git a/src/components/Workouts/WorkoutPagiation.tsx b/src/components/Workouts/WorkoutPagiation.tsx new file mode 100644 index 0000000..2b215d7 --- /dev/null +++ b/src/components/Workouts/WorkoutPagiation.tsx @@ -0,0 +1,36 @@ +'use client' + +import useWorkouts from '@/stores/Workouts' +import { Button } from '../ui/button' +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react' + +const WorkoutPagiation = () => { + const { workouts } = useWorkouts() + return ( + + ) +} + +export default WorkoutPagiation diff --git a/src/components/Workouts/WorkoutPopoutEditForm.tsx b/src/components/Workouts/WorkoutPopoutEditForm.tsx new file mode 100644 index 0000000..de8b1f3 --- /dev/null +++ b/src/components/Workouts/WorkoutPopoutEditForm.tsx @@ -0,0 +1,84 @@ +'use client' +import { Workout } from '@/payload-types' +import useWorkouts from '@/stores/Workouts' +import { + PopoverBody, + PopoverContent, + PopoverHeader, + PopoverRoot, + PopoverTrigger, +} from '../ui/popover' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '../ui/form' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { Input } from '../ui/input' +import { z } from 'zod' + +const editFormSchema = z.object({ + name: z.string().min(2, { + message: 'Workout name must be at least 2 characters.', + }), +}) + +type Props = { + workout: Workout +} +const WorkoutPopoutEditForm = (props: Props) => { + const w = props.workout + const form = useForm>({ + resolver: zodResolver(editFormSchema), + defaultValues: { + name: '', + }, + }) + + const handleEditSubmit = (e: any) => { + console.log(e) + } + + return ( + + +
+ {w.name} + + Edit + +
+
+ + Edit Workout + +
+ + ( + + Workout Name + + + + This is your public display name. + + + )} + /> + + +
+
+
+ ) +} + +export default WorkoutPopoutEditForm diff --git a/src/components/Workouts/WorkoutsListView.tsx b/src/components/Workouts/WorkoutsListView.tsx new file mode 100644 index 0000000..538548d --- /dev/null +++ b/src/components/Workouts/WorkoutsListView.tsx @@ -0,0 +1,94 @@ +'use client' + +import useWorkouts from '@/stores/Workouts' +import NeumorphButton from '../ui/neumorph-button' +import WorkoutPopoutEditForm from './WorkoutPopoutEditForm' +import { Media, Workout } from '@/payload-types' +import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar' +import makeAcronym from '@/utilities/makeAcronym' +import Link from 'next/link' +import { useCallback } from 'react' +import { ChevronLeftIcon, ChevronRightIcon, Edit } from 'lucide-react' +import { Button } from '../ui/button' + +const WorkoutListView = () => { + const { workouts, workoutTypes } = useWorkouts() + + const testWorkouts = { + docs: [ + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ...(workouts?.docs || []), + ], + } + + const getWorkoutTypeById = useCallback( + (typeId: number) => { + const foundWorkoutType = workoutTypes?.docs.find((t) => t.id === typeId) + return foundWorkoutType + }, + [workoutTypes, workouts], + ) + + return ( +
    + {testWorkouts?.docs.map((w, i) => { + const avatar = (w.avatar as Media) || undefined + return ( +
  • +
    + + + + {makeAcronym(w.name || '')} + + +
    +

    + {w.name} +

    +

    {w.description}

    +
    +
    +
    +
    +

    + {w.type + ?.map((t) => { + console.log(t) + if (typeof t === 'number') return getWorkoutTypeById(t)?.name + else return t.name + }) + .filter((t) => !!t) + .join(', ')} +

    + {!!w.durationMinutes && ( +

    + {w.durationMinutes} mins +

    + )} +
    + +
    +
  • + ) + })} +
+ ) +} + +export default WorkoutListView diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 65d4fcd..9fef397 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils/index" const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..94046f1 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils/index" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +