feat: workouts list view admin panel

This commit is contained in:
Yehoshua Sandler 2025-05-22 21:20:07 -05:00
parent 0f5f6409cc
commit 4b7901703b
20 changed files with 1448 additions and 75 deletions

284
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.0.1",
"@payloadcms/admin-bar": "3.38.0", "@payloadcms/admin-bar": "3.38.0",
"@payloadcms/db-postgres": "3.38.0", "@payloadcms/db-postgres": "3.38.0",
"@payloadcms/live-preview-react": "3.38.0", "@payloadcms/live-preview-react": "3.38.0",
@ -27,20 +28,22 @@
"@radix-ui/react-collapsible": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14", "@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-popover": "^1.1.13",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.6", "@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", "@radix-ui/react-tooltip": "^1.2.6",
"@tabler/icons-react": "^3.33.0", "@tabler/icons-react": "^3.33.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"framer-motion": "^12.12.1",
"geist": "^1.3.0", "geist": "^1.3.0",
"graphql": "^16.8.2", "graphql": "^16.8.2",
"lucide-react": "^0.378.0", "lucide-react": "^0.378.0",
"motion": "^12.12.1",
"next": "15.3.0", "next": "15.3.0",
"next-sitemap": "^4.2.3", "next-sitemap": "^4.2.3",
"payload": "3.38.0", "payload": "3.38.0",
@ -48,10 +51,12 @@
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "19.1.0", "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", "sharp": "0.32.6",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.25.7",
"zustand": "^5.0.4" "zustand": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {
@ -2493,6 +2498,18 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT" "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": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "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": { "node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "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": { "node_modules/@radix-ui/react-direction": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", "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": { "node_modules/@radix-ui/react-label": {
"version": "2.1.6", "version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.6.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
"integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==", "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "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": { "peerDependencies": {
"@types/react": "*", "@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": { "node_modules/@radix-ui/react-popover": {
"version": "1.1.13", "version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.13.tgz", "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": { "node_modules/@radix-ui/react-popper": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", "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": { "node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.9", "version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", "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": { "node_modules/@radix-ui/react-separator": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.6.tgz", "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": { "node_modules/@radix-ui/react-slot": {
"version": "1.2.2", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-compose-refs": "1.1.2" "@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": { "node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "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": ">=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": { "node_modules/@swc/counter": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@ -8937,6 +9109,33 @@
"url": "https://github.com/sponsors/rawify" "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": { "node_modules/fs-constants": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@ -11025,6 +11224,47 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -12773,6 +13013,21 @@
"react-dom": ">=16.6.0" "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": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -15391,6 +15646,15 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/zustand": {
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz",

View File

@ -19,6 +19,7 @@
"start": "cross-env NODE_OPTIONS=--no-deprecation next start" "start": "cross-env NODE_OPTIONS=--no-deprecation next start"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.0.1",
"@payloadcms/admin-bar": "3.38.0", "@payloadcms/admin-bar": "3.38.0",
"@payloadcms/db-postgres": "3.38.0", "@payloadcms/db-postgres": "3.38.0",
"@payloadcms/live-preview-react": "3.38.0", "@payloadcms/live-preview-react": "3.38.0",
@ -37,20 +38,22 @@
"@radix-ui/react-collapsible": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14", "@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-popover": "^1.1.13",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.1.6", "@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", "@radix-ui/react-tooltip": "^1.2.6",
"@tabler/icons-react": "^3.33.0", "@tabler/icons-react": "^3.33.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"framer-motion": "^12.12.1",
"geist": "^1.3.0", "geist": "^1.3.0",
"graphql": "^16.8.2", "graphql": "^16.8.2",
"lucide-react": "^0.378.0", "lucide-react": "^0.378.0",
"motion": "^12.12.1",
"next": "15.3.0", "next": "15.3.0",
"next-sitemap": "^4.2.3", "next-sitemap": "^4.2.3",
"payload": "3.38.0", "payload": "3.38.0",
@ -58,10 +61,12 @@
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "19.1.0", "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", "sharp": "0.32.6",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.25.7",
"zustand": "^5.0.4" "zustand": "^5.0.4"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { Exercise, ExerciseType, Workout } from '@/payload-types' import { Exercise, ExerciseType, Workout, WorkoutType } from '@/payload-types'
import useWorkouts from '@/stores/Workouts' import useWorkouts from '@/stores/Workouts'
import { PaginatedDocs } from 'payload' import { PaginatedDocs } from 'payload'
import { ReactNode, use, useEffect } from 'react' import { ReactNode, use, useEffect } from 'react'
@ -8,6 +8,7 @@ import { ReactNode, use, useEffect } from 'react'
type Props = { type Props = {
children: ReactNode children: ReactNode
getTenantWorkoutsPromise: Promise<PaginatedDocs<Workout>> getTenantWorkoutsPromise: Promise<PaginatedDocs<Workout>>
getTenantWorkoutTypesPromise: Promise<PaginatedDocs<WorkoutType>>
getTenantExercisesPromise: Promise<PaginatedDocs<Exercise>> getTenantExercisesPromise: Promise<PaginatedDocs<Exercise>>
getTenantExerciseTypesPromise: Promise<PaginatedDocs<ExerciseType>> getTenantExerciseTypesPromise: Promise<PaginatedDocs<ExerciseType>>
} }
@ -17,13 +18,15 @@ const WorkoutsLayoutSuspendedFrontend = (props: Props) => {
getTenantExerciseTypesPromise, getTenantExerciseTypesPromise,
getTenantExercisesPromise, getTenantExercisesPromise,
getTenantWorkoutsPromise, getTenantWorkoutsPromise,
getTenantWorkoutTypesPromise,
} = props } = props
const exerciseTypeResponse = use(getTenantExerciseTypesPromise) const exerciseTypeResponse = use(getTenantExerciseTypesPromise)
const exerciseResponse = use(getTenantExercisesPromise) const exerciseResponse = use(getTenantExercisesPromise)
const workoutResponse = use(getTenantWorkoutsPromise) const workoutResponse = use(getTenantWorkoutsPromise)
const workoutTypeResponse = use(getTenantWorkoutTypesPromise)
const { setExerciseTypes, setExercises, setWorkouts } = useWorkouts() const { setExerciseTypes, setExercises, setWorkouts, setWorkoutTypes } = useWorkouts()
useEffect(() => { useEffect(() => {
if (exerciseTypeResponse?.docs?.length) setExerciseTypes(exerciseTypeResponse) if (exerciseTypeResponse?.docs?.length) setExerciseTypes(exerciseTypeResponse)
@ -33,6 +36,10 @@ const WorkoutsLayoutSuspendedFrontend = (props: Props) => {
if (exerciseResponse?.docs?.length) setExercises(exerciseResponse) if (exerciseResponse?.docs?.length) setExercises(exerciseResponse)
}, [exerciseResponse]) }, [exerciseResponse])
useEffect(() => {
if (workoutTypeResponse?.docs?.length) setWorkoutTypes(workoutTypeResponse)
}, [workoutResponse])
useEffect(() => { useEffect(() => {
if (workoutResponse?.docs?.length) setWorkouts(workoutResponse) if (workoutResponse?.docs?.length) setWorkouts(workoutResponse)
}, [workoutResponse]) }, [workoutResponse])

View File

@ -2,7 +2,9 @@ import { ReactNode } from 'react'
import configPromise from '@payload-config' import configPromise from '@payload-config'
import { getPayload, PaginatedDocs } from 'payload' import { getPayload, PaginatedDocs } from 'payload'
import WorkoutsLayoutSuspendedFrontend from './layout.client' 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 = { type Props = {
params: Promise<{ params: Promise<{
@ -18,7 +20,7 @@ const WorkoutsLayout = async (props: Props) => {
const getExerciseTypesPromise = payload.find({ const getExerciseTypesPromise = payload.find({
collection: 'exerciseTypes', collection: 'exerciseTypes',
limit: 50, limit: TYPE_LIMIT,
depth: 0, depth: 0,
select: { select: {
id: true, id: true,
@ -51,6 +53,22 @@ const WorkoutsLayout = async (props: Props) => {
}, },
}) as Promise<PaginatedDocs<Exercise>> }) 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({ const getWorkoutsPromise = payload.find({
collection: 'workouts', collection: 'workouts',
limit: 20, limit: 20,
@ -68,12 +86,13 @@ const WorkoutsLayout = async (props: Props) => {
equals: tenantSlug, equals: tenantSlug,
}, },
}, },
}) as Promise<PaginatedDocs<ExerciseType>> }) as Promise<PaginatedDocs<Workout>>
return ( return (
<WorkoutsLayoutSuspendedFrontend <WorkoutsLayoutSuspendedFrontend
getTenantExerciseTypesPromise={getExerciseTypesPromise} getTenantExerciseTypesPromise={getExerciseTypesPromise}
getTenantExercisesPromise={getTenantExercisesPromise} getTenantExercisesPromise={getTenantExercisesPromise}
getTenantWorkoutTypesPromise={getWorkoutTypesPromise}
getTenantWorkoutsPromise={getWorkoutsPromise} getTenantWorkoutsPromise={getWorkoutsPromise}
> >
{children} {children}

View File

@ -1,13 +1,19 @@
import { DashboardContent, DashboardContentSection } from '@/components/Dashboard' import { DashboardContent, DashboardContentSection } from '@/components/Dashboard'
import WorkoutPagiation from '@/components/Workouts/WorkoutPagiation'
import WorkoutListView from '@/components/Workouts/WorkoutsListView'
// import WorkoutsPageClient from './page.client' // import WorkoutsPageClient from './page.client'
const WorkoutsPage = () => { const WorkoutsPage = () => {
return ( return (
<DashboardContent className="grid grid-cols-1 xl:grid-cols-2"> <DashboardContent className="grid grid-cols-1 xl:grid-cols-2">
<DashboardContentSection className="col-span-1 xl:max-w-lg md:ml-0"> <DashboardContentSection
<h1>Workouts</h1> 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>
<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> <h1>Exercises</h1>
</DashboardContentSection> </DashboardContentSection>
<DashboardContentSection className="col-span-1 xl:col-span-2"> <DashboardContentSection className="col-span-1 xl:col-span-2">

View File

@ -34,7 +34,7 @@
--muted: 210 40% 96.1%; --muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%; --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%; --accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
@ -44,7 +44,7 @@
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%; --ring: 222.2 84% 4.9%;
--radius: 0.2rem; --radius: 0.3rem;
--success: 196 52% 74%; --success: 196 52% 74%;
--warning: 34 89% 85%; --warning: 34 89% 85%;
@ -53,7 +53,7 @@
--sidebar-foreground: 240 5.3% 26.1%; --sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%; --sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%; --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-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%; --sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%; --sidebar-ring: 217.2 91.2% 59.8%;
@ -69,8 +69,8 @@
--popover: 222.2 84% 4.9%; --popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--primary: 210 40% 98%; --primary: 48 96.6% 76.7%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 240 4.8% 15.9%;
--secondary: 217.2 32.6% 17.5%; --secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 210 40% 98%;
@ -78,8 +78,8 @@
--muted: 217.2 32.6% 17.5%; --muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%; --accent: 48 96.6% 76.7%;
--accent-foreground: 210 40% 98%; --accent-foreground: 240 4.8% 15.9%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
@ -95,10 +95,10 @@
--sidebar-background: 240 5.9% 10%; --sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%; --sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%; --sidebar-primary: 48 96.6% 76.7%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 240 4.8% 15.9%;
--sidebar-accent: 240 3.7% 15.9%; --sidebar-accent: 48 96.6% 76.7%;
--sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-accent-foreground: 240 4.8% 15.9%;
--sidebar-border: 240 3.7% 15.9%; --sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%; --sidebar-ring: 217.2 91.2% 59.8%;
} }
@ -122,8 +122,8 @@
--popover: 222.2 84% 4.9%; --popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--primary: 210 40% 98%; --primary: 48 96.6% 76.7%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 240 4.8% 15.9%;
--secondary: 217.2 32.6% 17.5%; --secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 210 40% 98%;

View File

@ -76,6 +76,29 @@ export const Workouts: CollectionConfig = {
type: 'text', type: 'text',
unique: true, 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', name: 'type',
type: 'relationship', type: 'relationship',

View File

@ -5,13 +5,27 @@ import { ReactNode } from 'react'
type Props = { type Props = {
children: ReactNode children: ReactNode
className?: ClassValue className?: ClassValue
heading?: ReactNode
footing?: ReactNode
} }
const DashboardContentSection = (props: Props) => { const DashboardContentSection = (props: Props) => {
const { children, className } = props const { children, className, heading, footing } = props
return ( return (
<section className={cn('mx-auto w-full p-6 rounded-xl bg-muted/50', className || '')}> <section className={cn('relative mx-auto w-full rounded-xl bg-muted/50', className || '')}>
{children} {!!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> </section>
) )
} }

View 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

View 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

View 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

View File

@ -2,7 +2,7 @@ import * as React from "react"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils/index"
const buttonVariants = cva( 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", "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
View 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,
}

View File

@ -1,19 +1,26 @@
'use client' "use client"
import { cn } from '@/utilities/ui' import * as React from "react"
import * as LabelPrimitive from '@radix-ui/react-label' import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from 'class-variance-authority' import { cva, type VariantProps } from "class-variance-authority"
import * as React from 'react'
import { cn } from "@/lib/utils/index"
const labelVariants = cva( 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< const Label = React.forwardRef<
{ ref?: React.Ref<HTMLLabelElement> } & React.ComponentProps<typeof LabelPrimitive.Root> & React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants> VariantProps<typeof labelVariants>
> = ({ className, ref, ...props }) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root className={cn(labelVariants(), className)} ref={ref} {...props} /> <LabelPrimitive.Root
) ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label } export { Label }

View 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

View File

@ -1,33 +1,254 @@
"use client" 'use client'
import * as React from "react" import React, {
import * as PopoverPrimitive from "@radix-ui/react-popover" 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< interface PopoverContextType {
React.ElementRef<typeof PopoverPrimitive.Content>, isOpen: boolean
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> openPopover: () => void
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( closePopover: () => void
<PopoverPrimitive.Portal> uniqueId: string
<PopoverPrimitive.Content note: string
ref={ref} setNote: (note: string) => void
align={align} }
sideOffset={sideOffset}
className={cn( const PopoverContext = createContext<PopoverContextType | undefined>(undefined)
"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 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(
'absolute h-[240px] overflow-hidden border border-primary/10 bg-sidebar outline-none z-50 container rounded-lg',
className,
)}
style={{
top: 'auto', // Remove any top positioning
left: 'auto', // Remove any left positioning
transform: 'none', // Remove any transform
}}
>
{children}
</motion.div>
)} )}
{...props} </AnimatePresence>
/> )
</PopoverPrimitive.Portal> }
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
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>
)
}

View 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

View File

@ -892,6 +892,8 @@ export interface Workout {
id: number; id: number;
tenant?: (number | null) | Tenant; tenant?: (number | null) | Tenant;
name?: string | null; name?: string | null;
avatar?: (number | null) | Media;
displayImage?: (number | null) | Media;
type?: (number | WorkoutType)[] | null; type?: (number | WorkoutType)[] | null;
difficulty?: number | null; difficulty?: number | null;
description?: string | null; description?: string | null;
@ -1897,6 +1899,8 @@ export interface EquipmentsSelect<T extends boolean = true> {
export interface WorkoutsSelect<T extends boolean = true> { export interface WorkoutsSelect<T extends boolean = true> {
tenant?: T; tenant?: T;
name?: T; name?: T;
avatar?: T;
displayImage?: T;
type?: T; type?: T;
difficulty?: T; difficulty?: T;
description?: T; description?: T;

View File

@ -1,17 +1,19 @@
import { create } from 'zustand' import { create } from 'zustand'
import { Exercise, ExerciseType, Workout } from '@/payload-types' import { Exercise, ExerciseType, Workout, WorkoutType } from '@/payload-types'
import { PaginatedDocs } from 'payload' import { PaginatedDocs } from 'payload'
export type WorkoutsProps = { export type WorkoutsProps = {
exercises?: PaginatedDocs<Exercise>, exercises?: PaginatedDocs<Exercise>,
exerciseTypes?: PaginatedDocs<ExerciseType>, exerciseTypes?: PaginatedDocs<ExerciseType>,
workouts?: PaginatedDocs<Workout>, workouts?: PaginatedDocs<Workout>,
workoutTypes?: PaginatedDocs<WorkoutType>,
} }
export type WorkoutsMethods = { export type WorkoutsMethods = {
setExercises: (exercises?: PaginatedDocs<Exercise>) => void, setExercises: (exercises?: PaginatedDocs<Exercise>) => void,
setExerciseTypes: (exerciseTypes?: PaginatedDocs<ExerciseType>) => void, setExerciseTypes: (exerciseTypes?: PaginatedDocs<ExerciseType>) => void,
setWorkouts: (workouts?: PaginatedDocs<Workout>) => void, setWorkouts: (workouts?: PaginatedDocs<Workout>) => void,
setWorkoutTypes: (workoutTypes?: PaginatedDocs<WorkoutType>) => void,
} }
export type WorkoutsStore = WorkoutsProps & WorkoutsMethods export type WorkoutsStore = WorkoutsProps & WorkoutsMethods
@ -23,6 +25,7 @@ const useWorkouts = create<WorkoutsStore>((set) => ({
setExercises: (exercises?: PaginatedDocs<Exercise>) => set(() => ({ exercises: exercises })), setExercises: (exercises?: PaginatedDocs<Exercise>) => set(() => ({ exercises: exercises })),
setExerciseTypes: (exerciseTypes?: PaginatedDocs<ExerciseType>) => set(() => ({ exerciseTypes: exerciseTypes })), setExerciseTypes: (exerciseTypes?: PaginatedDocs<ExerciseType>) => set(() => ({ exerciseTypes: exerciseTypes })),
setWorkouts: (workouts?: PaginatedDocs<Workout>) => set(() => ({ workouts: workouts })), setWorkouts: (workouts?: PaginatedDocs<Workout>) => set(() => ({ workouts: workouts })),
setWorkoutTypes: (workoutTypes?: PaginatedDocs<WorkoutType>) => set(() => ({ workoutTypes: workoutTypes }))
})) }))
export default useWorkouts export default useWorkouts

View 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