feat: workouts list view admin panel
This commit is contained in:
parent
0f5f6409cc
commit
4b7901703b
284
package-lock.json
generated
284
package-lock.json
generated
@ -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",
|
||||
|
||||
11
package.json
11
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": {
|
||||
|
||||
@ -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<PaginatedDocs<Workout>>
|
||||
getTenantWorkoutTypesPromise: Promise<PaginatedDocs<WorkoutType>>
|
||||
getTenantExercisesPromise: Promise<PaginatedDocs<Exercise>>
|
||||
getTenantExerciseTypesPromise: Promise<PaginatedDocs<ExerciseType>>
|
||||
}
|
||||
@ -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])
|
||||
|
||||
@ -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<PaginatedDocs<Exercise>>
|
||||
|
||||
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<PaginatedDocs<WorkoutType>>
|
||||
|
||||
const getWorkoutsPromise = payload.find({
|
||||
collection: 'workouts',
|
||||
limit: 20,
|
||||
@ -68,12 +86,13 @@ const WorkoutsLayout = async (props: Props) => {
|
||||
equals: tenantSlug,
|
||||
},
|
||||
},
|
||||
}) as Promise<PaginatedDocs<ExerciseType>>
|
||||
}) as Promise<PaginatedDocs<Workout>>
|
||||
|
||||
return (
|
||||
<WorkoutsLayoutSuspendedFrontend
|
||||
getTenantExerciseTypesPromise={getExerciseTypesPromise}
|
||||
getTenantExercisesPromise={getTenantExercisesPromise}
|
||||
getTenantWorkoutTypesPromise={getWorkoutTypesPromise}
|
||||
getTenantWorkoutsPromise={getWorkoutsPromise}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -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 (
|
||||
<DashboardContent className="grid grid-cols-1 xl:grid-cols-2">
|
||||
<DashboardContentSection className="col-span-1 xl:max-w-lg md:ml-0">
|
||||
<h1>Workouts</h1>
|
||||
<DashboardContentSection
|
||||
className="col-span-1 xl:max-w-lg md:ml-0 md:max-h-[620px] md:overflow-auto"
|
||||
heading={<h2 className="pb-2 pt-4 mx-4">Workouts</h2>}
|
||||
footing={<WorkoutPagiation />}
|
||||
>
|
||||
<WorkoutListView />
|
||||
</DashboardContentSection>
|
||||
<DashboardContentSection className="col-span-1 xl:max-w-lg md:mr-0">
|
||||
<DashboardContentSection className="col-span-1 xl:max-w-lg md:mr-0 md:max-h-[620px] md:overflow-auto">
|
||||
<h1>Exercises</h1>
|
||||
</DashboardContentSection>
|
||||
<DashboardContentSection className="col-span-1 xl:col-span-2">
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 (
|
||||
<section className={cn('mx-auto w-full p-6 rounded-xl bg-muted/50', className || '')}>
|
||||
{children}
|
||||
<section className={cn('relative mx-auto w-full rounded-xl bg-muted/50', className || '')}>
|
||||
{!!heading && (
|
||||
<div className="sticky top-0 left-0 w-full bg-muted font-semibold text-xl z-10">
|
||||
{heading}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-2">{children}</div>
|
||||
|
||||
{!!footing && (
|
||||
<div className="sticky bottom-0 left-0 w-full bg-muted rounded-b-lg font-semibold text-xl z-10">
|
||||
{footing}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
36
src/components/Workouts/WorkoutPagiation.tsx
Normal file
36
src/components/Workouts/WorkoutPagiation.tsx
Normal file
@ -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 (
|
||||
<nav
|
||||
aria-label="Workout Pagination"
|
||||
className="flex items-center justify-between px-4 py-3 sm:px-6"
|
||||
>
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing <span className="font-medium">{workouts?.pagingCounter}</span> to{' '}
|
||||
<span className="font-medium">
|
||||
{(workouts?.pagingCounter || 0) + ((workouts?.limit || 0) - 1)}
|
||||
</span>{' '}
|
||||
of <span className="font-medium">{workouts?.totalDocs}</span> results
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-1 justify-between gap-1 sm:justify-end">
|
||||
<Button variant="ghost" disabled={!workouts?.hasPrevPage}>
|
||||
<ChevronLeftIcon aria-hidden="true" className="size-5 flex-none text-muted-foreground" />
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" disabled={!workouts?.hasNextPage}>
|
||||
<ChevronRightIcon aria-hidden="true" className="size-5 flex-none text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkoutPagiation
|
||||
84
src/components/Workouts/WorkoutPopoutEditForm.tsx
Normal file
84
src/components/Workouts/WorkoutPopoutEditForm.tsx
Normal file
@ -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<z.infer<typeof editFormSchema>>({
|
||||
resolver: zodResolver(editFormSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const handleEditSubmit = (e: any) => {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverRoot>
|
||||
<PopoverTrigger className="w-full">
|
||||
<div className="w-full flex items-center justify-between my-2">
|
||||
<span className="inline-block text-lg font-medium">{w.name}</span>
|
||||
<span className="inline-block text-xs py-0.5 px-1 border border-primary rounded-lg">
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="rounded-lg">
|
||||
<PopoverHeader>Edit Workout</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleEditSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Workout Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="shadcn" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>This is your public display name.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkoutPopoutEditForm
|
||||
94
src/components/Workouts/WorkoutsListView.tsx
Normal file
94
src/components/Workouts/WorkoutsListView.tsx
Normal file
@ -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 (
|
||||
<ul role="list" className="divide-y divide-sidebar-border">
|
||||
{testWorkouts?.docs.map((w, i) => {
|
||||
const avatar = (w.avatar as Media) || undefined
|
||||
return (
|
||||
<li key={(w.name || '') + i} className="px-4 py-4 sm:px-0 flex justify-between">
|
||||
<div className="flex min-w-0 gap-x-4 ">
|
||||
<Avatar className="flex-none size-12 rounded-full">
|
||||
<AvatarImage src={avatar?.url || ''} alt={w.name || ''} />
|
||||
<AvatarFallback className="rounded-full">
|
||||
{makeAcronym(w.name || '')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-auto">
|
||||
<p className="text-sm/6 font-semibold ">
|
||||
<Link href={''}>{w.name}</Link>
|
||||
</p>
|
||||
<p className="mt-1 flex text-xs/5 text-muted-foreground">{w.description} </p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-x-4">
|
||||
<div className="hidden sm:flex sm:flex-col sm:items-end">
|
||||
<p className="text-sm/6">
|
||||
{w.type
|
||||
?.map((t) => {
|
||||
console.log(t)
|
||||
if (typeof t === 'number') return getWorkoutTypeById(t)?.name
|
||||
else return t.name
|
||||
})
|
||||
.filter((t) => !!t)
|
||||
.join(', ')}
|
||||
</p>
|
||||
{!!w.durationMinutes && (
|
||||
<p className="mt-1 text-xs/5 text-gray-500">
|
||||
<span>{w.durationMinutes} mins</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
className="block rounded-lg"
|
||||
onClick={() => console.log('clicked: ', w.name)}
|
||||
>
|
||||
<Edit aria-hidden="true" className="size-5 flex-none text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkoutListView
|
||||
@ -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",
|
||||
|
||||
178
src/components/ui/form.tsx
Normal file
178
src/components/ui/form.tsx
Normal file
@ -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<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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 <FormField>")
|
||||
}
|
||||
|
||||
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<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
@ -1,19 +1,26 @@
|
||||
'use client'
|
||||
"use client"
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label: React.FC<
|
||||
{ ref?: React.Ref<HTMLLabelElement> } & React.ComponentProps<typeof LabelPrimitive.Root> &
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
> = ({ className, ref, ...props }) => (
|
||||
<LabelPrimitive.Root className={cn(labelVariants(), className)} ref={ref} {...props} />
|
||||
)
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
|
||||
120
src/components/ui/neumorph-button.tsx
Normal file
120
src/components/ui/neumorph-button.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import type React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { motion, type HTMLMotionProps } from 'framer-motion'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
const buttonVariants = cva(
|
||||
// Base styles
|
||||
'justify-center px-4 text-sm font-medium items-center transition-[box-shadow,background-color] disabled:cursor-not-allowed disabled:opacity-50 flex active:transition-none',
|
||||
{
|
||||
variants: {
|
||||
intent: {
|
||||
default: [
|
||||
'bg-[#36322F]',
|
||||
'text-[#fff]',
|
||||
'hover:enabled:bg-[#4a4542]',
|
||||
'disabled:bg-[#8c8885]',
|
||||
'[box-shadow:inset_0px_-2.108433723449707px_0px_0px_#171310,_0px_1.2048193216323853px_6.325301647186279px_0px_rgba(58,_33,_8,_58%)]',
|
||||
'hover:enabled:[box-shadow:inset_0px_-2.53012px_0px_0px_#171310,_0px_1.44578px_7.59036px_0px_rgba(58,_33,_8,_64%)]',
|
||||
'disabled:shadow-none',
|
||||
'active:bg-[#2A2724]',
|
||||
'active:[box-shadow:inset_0px_-1.5px_0px_0px_#171310,_0px_0.5px_2px_0px_rgba(58,_33,_8,_70%)]',
|
||||
],
|
||||
primary: [
|
||||
'bg-primary',
|
||||
'text-primary-foreground',
|
||||
'hover:enabled:bg-accent',
|
||||
'disabled:bg-[#9FC3F5]',
|
||||
'[box-shadow:inset_0px_-2.108433723449707px_0px_0px_hsla(var(--accent)),_0px_1.2048193216323853px_6.325301647186279px_0px_hsla(var(--accent))]',
|
||||
'hover:enabled:[box-shadow:inset_0px_-2.53012px_0px_0px_hsla(var(--accent)),_0px_1.44578px_7.59036px_0px_hsla(var(--accent))]',
|
||||
'disabled:shadow-none',
|
||||
'active:bg-[#1A68D1]',
|
||||
'active:[box-shadow:inset_0px_-1.5px_0px_0px_#1554AB,_0px_0.5px_2px_0px_rgba(28,_100,_242,_70%)]',
|
||||
],
|
||||
secondary: [
|
||||
'bg-[#FFFFFF]',
|
||||
'text-[#36322F]',
|
||||
'hover:enabled:bg-[#F8F8F8]',
|
||||
'disabled:bg-[#F0F0F0]',
|
||||
'[box-shadow:inset_0px_-2.108433723449707px_0px_0px_#E0E0E0,_0px_1.2048193216323853px_6.325301647186279px_0px_rgba(0,_0,_0,_10%)]',
|
||||
'hover:enabled:[box-shadow:inset_0px_-2.53012px_0px_0px_#E8E8E8,_0px_1.44578px_7.59036px_0px_rgba(0,_0,_0,_12%)]',
|
||||
'disabled:shadow-none',
|
||||
'border',
|
||||
'border-[#E0E0E0]',
|
||||
'active:bg-[#F0F0F0]',
|
||||
'active:[box-shadow:inset_0px_-1.5px_0px_0px_#D8D8D8,_0px_0.5px_2px_0px_rgba(0,_0,_0,_15%)]',
|
||||
],
|
||||
danger: [
|
||||
'bg-[#E6492D]',
|
||||
'text-[#fff]',
|
||||
'hover:enabled:bg-[#F05B41]',
|
||||
'disabled:bg-[#F5A799]',
|
||||
'[box-shadow:inset_0px_-2.108433723449707px_0px_0px_#D63A1F,_0px_1.2048193216323853px_6.325301647186279px_0px_rgba(214,_58,_31,_58%)]',
|
||||
'hover:enabled:[box-shadow:inset_0px_-2.53012px_0px_0px_#E6492D,_0px_1.44578px_7.59036px_0px_rgba(214,_58,_31,_64%)]',
|
||||
'disabled:shadow-none',
|
||||
'active:bg-[#D63A1F]',
|
||||
'active:[box-shadow:inset_0px_-1.5px_0px_0px_#B22E17,_0px_0.5px_2px_0px_rgba(214,_58,_31,_70%)]',
|
||||
],
|
||||
},
|
||||
size: {
|
||||
small: ['text-xs', 'py-1', 'px-2', 'h-9', 'rounded-[8px]'],
|
||||
medium: ['text-base', 'py-2', 'px-4', 'h-11', 'rounded-[9px]'],
|
||||
large: ['text-lg', 'py-3', 'px-6', 'h-14', 'rounded-[11px]'],
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
intent: ['default', 'primary', 'secondary', 'danger'],
|
||||
size: 'medium',
|
||||
className: 'uppercase',
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
intent: 'default',
|
||||
size: 'medium',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface NeumorphButtonProps
|
||||
extends HTMLMotionProps<'button'>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
children: React.ReactNode
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const NeumorphButton: React.FC<NeumorphButtonProps> = ({
|
||||
className,
|
||||
intent,
|
||||
size,
|
||||
fullWidth,
|
||||
children,
|
||||
loading = false,
|
||||
disabled,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<motion.button
|
||||
className={buttonVariants({ intent, size, fullWidth, className })}
|
||||
disabled={disabled || loading}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 10 }}
|
||||
{...props}
|
||||
>
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
<motion.span
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: loading ? 0.7 : 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{children}
|
||||
</motion.span>
|
||||
</motion.button>
|
||||
)
|
||||
}
|
||||
|
||||
export default NeumorphButton
|
||||
@ -1,33 +1,254 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import React, {
|
||||
createContext,
|
||||
HTMLAttributes,
|
||||
useContext,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { AnimatePresence, MotionConfig, motion } from 'motion/react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils/index'
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
const TRANSITION = {
|
||||
type: 'spring',
|
||||
bounce: 0.05,
|
||||
duration: 0.3,
|
||||
}
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
function useClickOutside(ref: React.RefObject<HTMLElement>, handler: () => void) {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
handler()
|
||||
}
|
||||
}
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [ref, handler])
|
||||
}
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
interface PopoverContextType {
|
||||
isOpen: boolean
|
||||
openPopover: () => void
|
||||
closePopover: () => void
|
||||
uniqueId: string
|
||||
note: string
|
||||
setNote: (note: string) => void
|
||||
}
|
||||
|
||||
const PopoverContext = createContext<PopoverContextType | undefined>(undefined)
|
||||
|
||||
function usePopover() {
|
||||
const context = useContext(PopoverContext)
|
||||
if (!context) {
|
||||
throw new Error('usePopover must be used within a PopoverProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
function usePopoverLogic() {
|
||||
const uniqueId = useId()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [note, setNote] = useState('')
|
||||
|
||||
const openPopover = () => setIsOpen(true)
|
||||
const closePopover = () => {
|
||||
setIsOpen(false)
|
||||
setNote('')
|
||||
}
|
||||
|
||||
return { isOpen, openPopover, closePopover, uniqueId, note, setNote }
|
||||
}
|
||||
|
||||
interface PopoverRootProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PopoverRoot({ children, className }: PopoverRootProps) {
|
||||
const popoverLogic = usePopoverLogic()
|
||||
|
||||
return (
|
||||
<PopoverContext.Provider value={popoverLogic}>
|
||||
<MotionConfig transition={TRANSITION}>
|
||||
<div className={cn('relative flex items-center justify-center isolate', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</MotionConfig>
|
||||
</PopoverContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
interface PopoverTriggerProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PopoverTrigger({ children, className }: PopoverTriggerProps) {
|
||||
const { openPopover, uniqueId } = usePopover()
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key="button"
|
||||
layoutId={`popover-${uniqueId}`}
|
||||
className={cn(className)}
|
||||
onClick={openPopover}
|
||||
>
|
||||
<motion.span layoutId={`popover-label-${uniqueId}`}>{children}</motion.span>
|
||||
</motion.button>
|
||||
)
|
||||
}
|
||||
|
||||
interface PopoverContentProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PopoverContent({ children, className }: PopoverContentProps) {
|
||||
const { isOpen, closePopover, uniqueId } = usePopover()
|
||||
const formContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useClickOutside(formContainerRef, closePopover)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
closePopover()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [closePopover])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
ref={formContainerRef}
|
||||
layoutId={`popover-${uniqueId}`}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
|
||||
className
|
||||
'absolute h-[240px] overflow-hidden border border-primary/10 bg-sidebar outline-none z-50 container rounded-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
style={{
|
||||
top: 'auto', // Remove any top positioning
|
||||
left: 'auto', // Remove any left positioning
|
||||
transform: 'none', // Remove any transform
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
interface PopoverFooterProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PopoverFooter({ children, className }: PopoverFooterProps) {
|
||||
return (
|
||||
<div key="close" className={cn('flex justify-between px-4 py-3', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PopoverCloseButtonProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PopoverCloseButton({ className }: PopoverCloseButtonProps) {
|
||||
const { closePopover } = usePopover()
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn('flex items-center', className)}
|
||||
onClick={closePopover}
|
||||
aria-label="Close popover"
|
||||
>
|
||||
<X size={16} className="text-zinc-900 dark:text-zinc-100" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
interface PopoverSubmitButtonProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PopoverSubmitButton({ className }: PopoverSubmitButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'relative ml-1 flex h-8 shrink-0 scale-100 select-none appearance-none items-center justify-center rounded-lg border border-zinc-950/10 bg-transparent px-2 text-sm text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98] dark:border-zinc-50/10 dark:text-zinc-50 dark:hover:bg-zinc-800',
|
||||
className,
|
||||
)}
|
||||
type="submit"
|
||||
aria-label="Submit note"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function PopoverHeader({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('px-4 py-2 font-semibold text-zinc-900 dark:text-zinc-100', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PopoverBody({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return <div className={cn('p-4', className)}>{children}</div>
|
||||
}
|
||||
|
||||
// New component: PopoverButton
|
||||
export function PopoverButton({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md px-4 py-2 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-700',
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
280
src/components/ui/text-animate.tsx
Normal file
280
src/components/ui/text-animate.tsx
Normal file
@ -0,0 +1,280 @@
|
||||
"use client"
|
||||
|
||||
import { FC, useEffect, useRef } from "react"
|
||||
import { HTMLMotionProps, motion, useAnimation, useInView } from "motion/react"
|
||||
|
||||
type AnimationType =
|
||||
| "fadeIn"
|
||||
| "fadeInUp"
|
||||
| "popIn"
|
||||
| "shiftInUp"
|
||||
| "rollIn"
|
||||
| "whipIn"
|
||||
| "whipInUp"
|
||||
| "calmInUp"
|
||||
|
||||
interface Props extends HTMLMotionProps<"div"> {
|
||||
text: string
|
||||
type?: AnimationType
|
||||
delay?: number
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const animationVariants = {
|
||||
fadeIn: {
|
||||
container: {
|
||||
hidden: { opacity: 0 },
|
||||
visible: (i: number = 1) => ({
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.05, delayChildren: i * 0.3 },
|
||||
}),
|
||||
},
|
||||
child: {
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: [0, -10, 0],
|
||||
transition: {
|
||||
type: "spring",
|
||||
damping: 12,
|
||||
stiffness: 100,
|
||||
},
|
||||
},
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
},
|
||||
},
|
||||
fadeInUp: {
|
||||
container: {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.1, delayChildren: 0.2 },
|
||||
},
|
||||
},
|
||||
child: {
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.5 } },
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
},
|
||||
},
|
||||
popIn: {
|
||||
container: {
|
||||
hidden: { scale: 0 },
|
||||
visible: {
|
||||
scale: 1,
|
||||
transition: { staggerChildren: 0.05, delayChildren: 0.2 },
|
||||
},
|
||||
},
|
||||
child: {
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1.1,
|
||||
transition: { type: "spring", damping: 15, stiffness: 400 },
|
||||
},
|
||||
hidden: { opacity: 0, scale: 0 },
|
||||
},
|
||||
},
|
||||
calmInUp: {
|
||||
container: {
|
||||
hidden: {},
|
||||
visible: (i: number = 1) => ({
|
||||
transition: { staggerChildren: 0.01, delayChildren: 0.2 * i },
|
||||
}),
|
||||
},
|
||||
child: {
|
||||
hidden: {
|
||||
y: "200%",
|
||||
transition: { ease: [0.455, 0.03, 0.515, 0.955], duration: 0.85 },
|
||||
},
|
||||
visible: {
|
||||
y: 0,
|
||||
transition: {
|
||||
ease: [0.125, 0.92, 0.69, 0.975], // Drawing attention to dynamic content or interactive elements, where the animation needs to be engaging but not abrupt
|
||||
duration: 0.75,
|
||||
// ease: [0.455, 0.03, 0.515, 0.955], // smooth and gradual acceleration followed by a steady deceleration towards the end of the animation
|
||||
// ease: [0.115, 0.955, 0.655, 0.939], // smooth and gradual acceleration followed by a steady deceleration towards the end of the animation
|
||||
// ease: [0.09, 0.88, 0.68, 0.98], // Very Gentle Onset, Swift Mid-Section, Soft Landing
|
||||
// ease: [0.11, 0.97, 0.64, 0.945], // Minimal Start, Energetic Acceleration, Smooth Closure
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shiftInUp: {
|
||||
container: {
|
||||
hidden: {},
|
||||
visible: (i: number = 1) => ({
|
||||
transition: { staggerChildren: 0.01, delayChildren: 0.2 * i },
|
||||
}),
|
||||
},
|
||||
child: {
|
||||
hidden: {
|
||||
y: "100%", // Starting from below but not too far to ensure a dramatic but manageable shift.
|
||||
transition: {
|
||||
ease: [0.75, 0, 0.25, 1], // Starting quickly
|
||||
duration: 0.6, // Shortened duration for a more dramatic start
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.8, // Slightly longer to accommodate the slow middle and swift end
|
||||
ease: [0.22, 1, 0.36, 1], // This easing function starts quickly (dramatic shift), slows down (slow middle), and ends quickly (clean swift end)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
whipInUp: {
|
||||
container: {
|
||||
hidden: {},
|
||||
visible: (i: number = 1) => ({
|
||||
transition: { staggerChildren: 0.01, delayChildren: 0.2 * i },
|
||||
}),
|
||||
},
|
||||
child: {
|
||||
hidden: {
|
||||
y: "200%",
|
||||
transition: { ease: [0.455, 0.03, 0.515, 0.955], duration: 0.45 },
|
||||
},
|
||||
visible: {
|
||||
y: 0,
|
||||
transition: {
|
||||
ease: [0.5, -0.15, 0.25, 1.05],
|
||||
duration: 0.75,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
rollIn: {
|
||||
container: {
|
||||
hidden: {},
|
||||
visible: {},
|
||||
},
|
||||
child: {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: `0.25em`,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: `0em`,
|
||||
transition: {
|
||||
duration: 0.65,
|
||||
ease: [0.65, 0, 0.75, 1], // Great! Swift Beginning, Prolonged Ease, Quick Finish
|
||||
// ease: [0.75, 0.05, 0.85, 1], // Quick Start, Smooth Middle, Sharp End
|
||||
// ease: [0.7, -0.25, 0.9, 1.25], // Fast Acceleration, Gentle Slowdown, Sudden Snap
|
||||
// ease: [0.7, -0.5, 0.85, 1.5], // Quick Leap, Soft Glide, Snappy Closure
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whipIn: {
|
||||
container: {
|
||||
hidden: {},
|
||||
visible: {},
|
||||
},
|
||||
child: {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: `0.35em`,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: `0em`,
|
||||
transition: {
|
||||
duration: 0.45,
|
||||
// ease: [0.75, 0.05, 0.85, 1], // Quick Start, Smooth Middle, Sharp End
|
||||
// ease: [0.7, -0.25, 0.9, 1.25], // Fast Acceleration, Gentle Slowdown, Sudden Snap
|
||||
// ease: [0.65, 0, 0.75, 1], // Great! Swift Beginning, Prolonged Ease, Quick Finish
|
||||
ease: [0.85, 0.1, 0.9, 1.2], // Rapid Initiation, Subtle Slow, Sharp Conclusion
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const TextAnimate: FC<Props> = ({
|
||||
text,
|
||||
type = "whipInUp",
|
||||
...props
|
||||
}: Props) => {
|
||||
// const { ref, inView } = useInView({
|
||||
// threshold: 0.5,
|
||||
// triggerOnce: true,
|
||||
// });
|
||||
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true })
|
||||
|
||||
const letters = Array.from(text)
|
||||
const { container, child } = animationVariants[type]
|
||||
|
||||
const ctrls = useAnimation()
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isInView) {
|
||||
// ctrls.start("visible");
|
||||
// }
|
||||
// if (!isInView) {
|
||||
// ctrls.start("hidden");
|
||||
// }
|
||||
// }, [ctrls, isInView]);
|
||||
|
||||
if (type === "rollIn" || type === "whipIn") {
|
||||
return (
|
||||
<h2 className="mt-10 text-3xl font-black text-black dark:text-neutral-100 py-5 pb-8 px-8 md:text-5xl">
|
||||
{text.split(" ").map((word, index) => {
|
||||
return (
|
||||
<motion.span
|
||||
ref={ref}
|
||||
className="inline-block mr-[0.25em] whitespace-nowrap"
|
||||
aria-hidden="true"
|
||||
key={index}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={container}
|
||||
transition={{
|
||||
delayChildren: index * 0.13,
|
||||
// delayChildren: index * 0.35,
|
||||
staggerChildren: 0.025,
|
||||
// staggerChildren: 0.05,
|
||||
}}
|
||||
>
|
||||
{word.split("").map((character, index) => {
|
||||
return (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
key={index}
|
||||
variants={child}
|
||||
className="inline-block -mr-[0.01em]"
|
||||
>
|
||||
{character}
|
||||
</motion.span>
|
||||
)
|
||||
})}
|
||||
</motion.span>
|
||||
)
|
||||
})}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.h2
|
||||
style={{ display: "flex", overflow: "hidden" }}
|
||||
role="heading"
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="mt-10 text-4xl font-black text-black dark:text-neutral-100 py-5 pb-8 px-8 md:text-5xl"
|
||||
{...props}
|
||||
>
|
||||
{letters.map((letter, index) => (
|
||||
<motion.span key={index} variants={child}>
|
||||
{letter === " " ? "\u00A0" : letter}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.h2>
|
||||
)
|
||||
}
|
||||
|
||||
export { TextAnimate }
|
||||
export default TextAnimate
|
||||
@ -892,6 +892,8 @@ export interface Workout {
|
||||
id: number;
|
||||
tenant?: (number | null) | Tenant;
|
||||
name?: string | null;
|
||||
avatar?: (number | null) | Media;
|
||||
displayImage?: (number | null) | Media;
|
||||
type?: (number | WorkoutType)[] | null;
|
||||
difficulty?: number | null;
|
||||
description?: string | null;
|
||||
@ -1897,6 +1899,8 @@ export interface EquipmentsSelect<T extends boolean = true> {
|
||||
export interface WorkoutsSelect<T extends boolean = true> {
|
||||
tenant?: T;
|
||||
name?: T;
|
||||
avatar?: T;
|
||||
displayImage?: T;
|
||||
type?: T;
|
||||
difficulty?: T;
|
||||
description?: T;
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import { create } from 'zustand'
|
||||
import { Exercise, ExerciseType, Workout } from '@/payload-types'
|
||||
import { Exercise, ExerciseType, Workout, WorkoutType } from '@/payload-types'
|
||||
import { PaginatedDocs } from 'payload'
|
||||
|
||||
export type WorkoutsProps = {
|
||||
exercises?: PaginatedDocs<Exercise>,
|
||||
exerciseTypes?: PaginatedDocs<ExerciseType>,
|
||||
workouts?: PaginatedDocs<Workout>,
|
||||
workoutTypes?: PaginatedDocs<WorkoutType>,
|
||||
}
|
||||
|
||||
export type WorkoutsMethods = {
|
||||
setExercises: (exercises?: PaginatedDocs<Exercise>) => void,
|
||||
setExerciseTypes: (exerciseTypes?: PaginatedDocs<ExerciseType>) => void,
|
||||
setWorkouts: (workouts?: PaginatedDocs<Workout>) => void,
|
||||
setWorkoutTypes: (workoutTypes?: PaginatedDocs<WorkoutType>) => void,
|
||||
}
|
||||
|
||||
export type WorkoutsStore = WorkoutsProps & WorkoutsMethods
|
||||
@ -23,6 +25,7 @@ const useWorkouts = create<WorkoutsStore>((set) => ({
|
||||
setExercises: (exercises?: PaginatedDocs<Exercise>) => set(() => ({ exercises: exercises })),
|
||||
setExerciseTypes: (exerciseTypes?: PaginatedDocs<ExerciseType>) => set(() => ({ exerciseTypes: exerciseTypes })),
|
||||
setWorkouts: (workouts?: PaginatedDocs<Workout>) => set(() => ({ workouts: workouts })),
|
||||
setWorkoutTypes: (workoutTypes?: PaginatedDocs<WorkoutType>) => set(() => ({ workoutTypes: workoutTypes }))
|
||||
}))
|
||||
|
||||
export default useWorkouts
|
||||
|
||||
8
src/utilities/makeAcronym.ts
Normal file
8
src/utilities/makeAcronym.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const makeAcronym = (name: string, maxLength: number = 2) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((part) => part[0])
|
||||
.slice(0, maxLength)
|
||||
}
|
||||
|
||||
export default makeAcronym
|
||||
Loading…
x
Reference in New Issue
Block a user