diff --git a/components.json b/components.json index 24e4297..ac52552 100644 --- a/components.json +++ b/components.json @@ -1,10 +1,10 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", + "style": "new-york", "rsc": true, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "tailwind.config.mjs", "css": "src/app/(frontend)/globals.css", "baseColor": "slate", "cssVariables": true, @@ -12,6 +12,10 @@ }, "aliases": { "components": "@/components", - "utils": "@/utilities/ui" - } + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" } diff --git a/package-lock.json b/package-lock.json index 2365e94..8246793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,21 +22,31 @@ "@payloadcms/plugin-seo": "3.38.0", "@payloadcms/richtext-lexical": "3.38.0", "@payloadcms/ui": "3.38.0", + "@radix-ui/react-avatar": "^1.1.9", "@radix-ui/react-checkbox": "^1.0.4", + "@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-popover": "^1.1.13", "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-slot": "^1.0.2", - "class-variance-authority": "^0.7.0", + "@radix-ui/react-separator": "^1.1.6", + "@radix-ui/react-slot": "^1.2.2", + "@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", "geist": "^1.3.0", "graphql": "^16.8.2", "lucide-react": "^0.378.0", "next": "15.3.0", "next-sitemap": "^4.2.3", - "payload": "3.33.0", + "payload": "3.38.0", "prism-react-renderer": "^2.3.1", "react": "19.1.0", + "react-day-picker": "^8.10.1", "react-dom": "19.1.0", "react-hook-form": "7.45.4", "sharp": "0.32.6", @@ -3950,6 +3960,33 @@ } } }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.9.tgz", + "integrity": "sha512-10tQokfvZdFvnvDkcOJPjm2pWiP8A0R4T83MoD7tb15bC/k2GU7B1YBuzJi8lNQ8V1QqhP8ocNqp27ByZaNagQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-checkbox": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.1.tgz", @@ -3980,6 +4017,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.10.tgz", + "integrity": "sha512-O2mcG3gZNkJ/Ena34HurA3llPOEA/M4dJtIRMa6y/cknRDC8XY5UZBInKTsUwW5cUue9A4k0wi1XU5fKBzKe1w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", @@ -4036,6 +4103,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz", + "integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -4078,6 +4181,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.14.tgz", + "integrity": "sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.14", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", @@ -4159,6 +4291,83 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.14.tgz", + "integrity": "sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-roving-focus": "1.1.9", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.13.tgz", + "integrity": "sha512-84uqQV3omKDR076izYgcha6gdpN8m3z6w/AeJ83MSBJYVG/AbOHdLjAgsPZkeC/kt+k64moXFCnio8BbqXszlw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", @@ -4262,6 +4471,37 @@ } } }, + "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", + "integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz", @@ -4305,6 +4545,29 @@ } } }, + "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", + "integrity": "sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", @@ -4323,6 +4586,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.6.tgz", + "integrity": "sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-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", @@ -4393,6 +4690,24 @@ } } }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "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-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", @@ -5248,6 +5563,32 @@ "tslib": "^2.8.0" } }, + "node_modules/@tabler/icons": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.33.0.tgz", + "integrity": "sha512-NZeFfzcYe7xcBHR3zKoCSrw/cFWvfj6LjenPQ48yVMTGdX854HH9nH44ZfMH8rrDzHBllfjwl4CIX6Vh2tyN0Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.33.0.tgz", + "integrity": "sha512-ay+HDecCjmFl25Lg14hcl59ffSjnOcgfrlV14shu8Qjbz+Xh4LRus93DuoyLQte8YSxE7Pe5gnEz6OF0GtwNtw==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.33.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", @@ -9046,13 +9387,10 @@ } }, "node_modules/image-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz", - "integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, @@ -11357,13 +11695,13 @@ } }, "node_modules/payload": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/payload/-/payload-3.33.0.tgz", - "integrity": "sha512-KXNQf+QFStxNUw4A/poEq8KP0GIR9K5tXzyMN80iwi1+X6V2xEORXs3hm28OQYIuJR4WTHBQzo7VWcQ69oNDcg==", + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/payload/-/payload-3.38.0.tgz", + "integrity": "sha512-A+KpKyn05Y4vlWTH4uAxkMySfBw4rtES1GydzFtjYEN8gxht1yXiK6zhfldGPyJrFuwPkzVpYC/7Lvg20Np7EA==", "license": "MIT", "dependencies": { "@next/env": "^15.1.5", - "@payloadcms/translations": "3.33.0", + "@payloadcms/translations": "3.38.0", "@types/busboy": "1.5.4", "ajv": "8.17.1", "bson-objectid": "2.0.4", @@ -11376,7 +11714,7 @@ "file-type": "19.3.0", "get-tsconfig": "4.8.1", "http-status": "2.1.0", - "image-size": "1.2.0", + "image-size": "2.0.2", "jose": "5.9.6", "json-schema-to-typescript": "15.0.3", "minimist": "1.2.8", @@ -11402,15 +11740,6 @@ "graphql": "^16.8.1" } }, - "node_modules/payload/node_modules/@payloadcms/translations": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/@payloadcms/translations/-/translations-3.33.0.tgz", - "integrity": "sha512-QU8EFoporBLdyeGWoWogb6gZMEMopvFHmF43yQDa2TrzWxGtEaSY5pNCvJovQN1aPnLPYaK4we/nX/3ifa4G5Q==", - "license": "MIT", - "dependencies": { - "date-fns": "4.1.0" - } - }, "node_modules/payload/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -12158,15 +12487,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12251,6 +12571,20 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-diff-viewer-continued": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-4.0.5.tgz", @@ -14715,6 +15049,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", diff --git a/package.json b/package.json index e7dc5f1..9125b37 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "biotracker", "version": "1.0.0", - "description": "Website template for Payload", + "description": "Iron City Biomechanics: Fitness Tracker for Instructors and Clients", "license": "MIT", "type": "module", "scripts": { @@ -32,21 +32,31 @@ "@payloadcms/plugin-seo": "3.38.0", "@payloadcms/richtext-lexical": "3.38.0", "@payloadcms/ui": "3.38.0", + "@radix-ui/react-avatar": "^1.1.9", "@radix-ui/react-checkbox": "^1.0.4", + "@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-popover": "^1.1.13", "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-slot": "^1.0.2", - "class-variance-authority": "^0.7.0", + "@radix-ui/react-separator": "^1.1.6", + "@radix-ui/react-slot": "^1.2.2", + "@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", "geist": "^1.3.0", "graphql": "^16.8.2", "lucide-react": "^0.378.0", "next": "15.3.0", "next-sitemap": "^4.2.3", - "payload": "3.33.0", + "payload": "3.38.0", "prism-react-renderer": "^2.3.1", "react": "19.1.0", + "react-day-picker": "^8.10.1", "react-dom": "19.1.0", "react-hook-form": "7.45.4", "sharp": "0.32.6", diff --git a/src/app/(frontend)/globals.css b/src/app/(frontend)/globals.css index 2c785c1..7178694 100644 --- a/src/app/(frontend)/globals.css +++ b/src/app/(frontend)/globals.css @@ -47,6 +47,14 @@ --success: 196 52% 74%; --warning: 34 89% 85%; --error: 10 100% 86%; + --sidebar-background: 0 0% 98%; + --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-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } [data-theme='dark'] { @@ -82,6 +90,16 @@ --warning: 34 51% 25%; --error: 10 39% 43%; } + .dark { + --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-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } } @layer base { @@ -101,3 +119,14 @@ html[data-theme='dark'], html[data-theme='light'] { opacity: initial; } + + + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..e6fe49d --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,44 @@ +import { SidebarLeft } from "@/components/sidebar-left" +import { SidebarRight } from "@/components/sidebar-right" +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" + +export default function Page() { + return ( + + + + + + + + + + + + Project Management & Task Tracking + + + + + + + + + + + + + + ) +} diff --git a/src/components/calendars.tsx b/src/components/calendars.tsx new file mode 100644 index 0000000..9a14ffa --- /dev/null +++ b/src/components/calendars.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { Check, ChevronRight } from "lucide-react" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarSeparator, +} from "@/components/ui/sidebar" + +export function Calendars({ + calendars, +}: { + calendars: { + name: string + items: string[] + }[] +}) { + return ( + <> + {calendars.map((calendar, index) => ( + + + + + + {calendar.name}{" "} + + + + + + + {calendar.items.map((item, index) => ( + + + + + + {item} + + + ))} + + + + + + + + ))} + > + ) +} diff --git a/src/components/date-picker.tsx b/src/components/date-picker.tsx new file mode 100644 index 0000000..5abd465 --- /dev/null +++ b/src/components/date-picker.tsx @@ -0,0 +1,15 @@ +import { Calendar } from "@/components/ui/calendar" +import { + SidebarGroup, + SidebarGroupContent, +} from "@/components/ui/sidebar" + +export function DatePicker() { + return ( + + + + + + ) +} diff --git a/src/components/nav-favorites.tsx b/src/components/nav-favorites.tsx new file mode 100644 index 0000000..34e2aea --- /dev/null +++ b/src/components/nav-favorites.tsx @@ -0,0 +1,94 @@ +"use client" + +import { + ArrowUpRight, + Link, + MoreHorizontal, + StarOff, + Trash2, +} from "lucide-react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" + +export function NavFavorites({ + favorites, +}: { + favorites: { + name: string + url: string + emoji: string + }[] +}) { + const { isMobile } = useSidebar() + + return ( + + Favorites + + {favorites.map((item) => ( + + + + {item.emoji} + {item.name} + + + + + + + More + + + + + + Remove from Favorites + + + + + Copy Link + + + + Open in New Tab + + + + + Delete + + + + + ))} + + + + More + + + + + ) +} diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx new file mode 100644 index 0000000..31edc97 --- /dev/null +++ b/src/components/nav-main.tsx @@ -0,0 +1,35 @@ +"use client" + +import { type LucideIcon } from "lucide-react" + +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +export function NavMain({ + items, +}: { + items: { + title: string + url: string + icon: LucideIcon + isActive?: boolean + }[] +}) { + return ( + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + ) +} diff --git a/src/components/nav-secondary.tsx b/src/components/nav-secondary.tsx new file mode 100644 index 0000000..71d2ebf --- /dev/null +++ b/src/components/nav-secondary.tsx @@ -0,0 +1,43 @@ +import React from "react" +import { type LucideIcon } from "lucide-react" + +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +export function NavSecondary({ + items, + ...props +}: { + items: { + title: string + url: string + icon: LucideIcon + badge?: React.ReactNode + }[] +} & React.ComponentPropsWithoutRef) { + return ( + + + + {items.map((item) => ( + + + + + {item.title} + + + {item.badge && {item.badge}} + + ))} + + + + ) +} diff --git a/src/components/nav-user.tsx b/src/components/nav-user.tsx new file mode 100644 index 0000000..239d4ba --- /dev/null +++ b/src/components/nav-user.tsx @@ -0,0 +1,114 @@ +"use client" + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from "lucide-react" + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" + +export function NavUser({ + user, +}: { + user: { + name: string + email: string + avatar: string + } +}) { + const { isMobile } = useSidebar() + + return ( + + + + + + + + CN + + + {user.name} + {user.email} + + + + + + + + + + CN + + + {user.name} + {user.email} + + + + + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + + + + + + ) +} diff --git a/src/components/nav-workspaces.tsx b/src/components/nav-workspaces.tsx new file mode 100644 index 0000000..a9627f1 --- /dev/null +++ b/src/components/nav-workspaces.tsx @@ -0,0 +1,85 @@ +import { ChevronRight, MoreHorizontal, Plus } from "lucide-react" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@/components/ui/sidebar" + +export function NavWorkspaces({ + workspaces, +}: { + workspaces: { + name: string + emoji: React.ReactNode + pages: { + name: string + emoji: React.ReactNode + }[] + }[] +}) { + return ( + + Workspaces + + + {workspaces.map((workspace) => ( + + + + + {workspace.emoji} + {workspace.name} + + + + + + + + + + + + + {workspace.pages.map((page) => ( + + + + {page.emoji} + {page.name} + + + + ))} + + + + + ))} + + + + More + + + + + + ) +} diff --git a/src/components/sidebar-left.tsx b/src/components/sidebar-left.tsx new file mode 100644 index 0000000..e5e52ce --- /dev/null +++ b/src/components/sidebar-left.tsx @@ -0,0 +1,278 @@ +"use client" + +import * as React from "react" +import { + AudioWaveform, + Blocks, + Calendar, + Command, + Home, + Inbox, + MessageCircleQuestion, + Search, + Settings2, + Sparkles, + Trash2, +} from "lucide-react" + +import { NavFavorites } from "@/components/nav-favorites" +import { NavMain } from "@/components/nav-main" +import { NavSecondary } from "@/components/nav-secondary" +import { NavWorkspaces } from "@/components/nav-workspaces" +import { TeamSwitcher } from "@/components/team-switcher" +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarRail, +} from "@/components/ui/sidebar" + +// This is sample data. +const data = { + teams: [ + { + name: "Acme Inc", + logo: Command, + plan: "Enterprise", + }, + { + name: "Acme Corp.", + logo: AudioWaveform, + plan: "Startup", + }, + { + name: "Evil Corp.", + logo: Command, + plan: "Free", + }, + ], + navMain: [ + { + title: "Search", + url: "#", + icon: Search, + }, + { + title: "Ask AI", + url: "#", + icon: Sparkles, + }, + { + title: "Home", + url: "#", + icon: Home, + isActive: true, + }, + { + title: "Inbox", + url: "#", + icon: Inbox, + badge: "10", + }, + ], + navSecondary: [ + { + title: "Calendar", + url: "#", + icon: Calendar, + }, + { + title: "Settings", + url: "#", + icon: Settings2, + }, + { + title: "Templates", + url: "#", + icon: Blocks, + }, + { + title: "Trash", + url: "#", + icon: Trash2, + }, + { + title: "Help", + url: "#", + icon: MessageCircleQuestion, + }, + ], + favorites: [ + { + name: "Project Management & Task Tracking", + url: "#", + emoji: "π", + }, + { + name: "Family Recipe Collection & Meal Planning", + url: "#", + emoji: "π³", + }, + { + name: "Fitness Tracker & Workout Routines", + url: "#", + emoji: "πͺ", + }, + { + name: "Book Notes & Reading List", + url: "#", + emoji: "π", + }, + { + name: "Sustainable Gardening Tips & Plant Care", + url: "#", + emoji: "π±", + }, + { + name: "Language Learning Progress & Resources", + url: "#", + emoji: "π£οΈ", + }, + { + name: "Home Renovation Ideas & Budget Tracker", + url: "#", + emoji: "π ", + }, + { + name: "Personal Finance & Investment Portfolio", + url: "#", + emoji: "π°", + }, + { + name: "Movie & TV Show Watchlist with Reviews", + url: "#", + emoji: "π¬", + }, + { + name: "Daily Habit Tracker & Goal Setting", + url: "#", + emoji: "β ", + }, + ], + workspaces: [ + { + name: "Personal Life Management", + emoji: "π ", + pages: [ + { + name: "Daily Journal & Reflection", + url: "#", + emoji: "π", + }, + { + name: "Health & Wellness Tracker", + url: "#", + emoji: "π", + }, + { + name: "Personal Growth & Learning Goals", + url: "#", + emoji: "π", + }, + ], + }, + { + name: "Professional Development", + emoji: "πΌ", + pages: [ + { + name: "Career Objectives & Milestones", + url: "#", + emoji: "π―", + }, + { + name: "Skill Acquisition & Training Log", + url: "#", + emoji: "π§ ", + }, + { + name: "Networking Contacts & Events", + url: "#", + emoji: "π€", + }, + ], + }, + { + name: "Creative Projects", + emoji: "π¨", + pages: [ + { + name: "Writing Ideas & Story Outlines", + url: "#", + emoji: "βοΈ", + }, + { + name: "Art & Design Portfolio", + url: "#", + emoji: "πΌοΈ", + }, + { + name: "Music Composition & Practice Log", + url: "#", + emoji: "π΅", + }, + ], + }, + { + name: "Home Management", + emoji: "π‘", + pages: [ + { + name: "Household Budget & Expense Tracking", + url: "#", + emoji: "π°", + }, + { + name: "Home Maintenance Schedule & Tasks", + url: "#", + emoji: "π§", + }, + { + name: "Family Calendar & Event Planning", + url: "#", + emoji: "π ", + }, + ], + }, + { + name: "Travel & Adventure", + emoji: "π§³", + pages: [ + { + name: "Trip Planning & Itineraries", + url: "#", + emoji: "πΊοΈ", + }, + { + name: "Travel Bucket List & Inspiration", + url: "#", + emoji: "π", + }, + { + name: "Travel Journal & Photo Gallery", + url: "#", + emoji: "πΈ", + }, + ], + }, + ], +} + +export function SidebarLeft({ + ...props +}: React.ComponentProps) { + return ( + + + + + + + + + + + + + ) +} diff --git a/src/components/sidebar-right.tsx b/src/components/sidebar-right.tsx new file mode 100644 index 0000000..c28d8ca --- /dev/null +++ b/src/components/sidebar-right.tsx @@ -0,0 +1,71 @@ +import * as React from "react" +import { Plus } from "lucide-react" + +import { Calendars } from "@/components/calendars" +import { DatePicker } from "@/components/date-picker" +import { NavUser } from "@/components/nav-user" +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + SidebarSeparator, +} from "@/components/ui/sidebar" + +// This is sample data. +const data = { + user: { + name: "shadcn", + email: "m@example.com", + avatar: "/avatars/shadcn.jpg", + }, + calendars: [ + { + name: "My Calendars", + items: ["Personal", "Work", "Family"], + }, + { + name: "Favorites", + items: ["Holidays", "Birthdays"], + }, + { + name: "Other", + items: ["Travel", "Reminders", "Deadlines"], + }, + ], +} + +export function SidebarRight({ + ...props +}: React.ComponentProps) { + return ( + + + + + + + + + + + + + + + New Calendar + + + + + + ) +} diff --git a/src/components/team-switcher.tsx b/src/components/team-switcher.tsx new file mode 100644 index 0000000..e0ecfab --- /dev/null +++ b/src/components/team-switcher.tsx @@ -0,0 +1,83 @@ +"use client" + +import * as React from "react" +import { ChevronDown, Plus } from "lucide-react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" + +export function TeamSwitcher({ + teams, +}: { + teams: { + name: string + logo: React.ElementType + plan: string + }[] +}) { + const [activeTeam, setActiveTeam] = React.useState(teams[0]) + + if (!activeTeam) { + return null + } + + return ( + + + + + + + + + {activeTeam.name} + + + + + + Teams + + {teams.map((team, index) => ( + setActiveTeam(team)} + className="gap-2 p-2" + > + + + + {team.name} + β{index + 1} + + ))} + + + + + + Add team + + + + + + ) +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) => ) +Breadcrumb.displayName = "Breadcrumb" + +const BreadcrumbList = React.forwardRef< + HTMLOListElement, + React.ComponentPropsWithoutRef<"ol"> +>(({ className, ...props }, ref) => ( + +)) +BreadcrumbList.displayName = "BreadcrumbList" + +const BreadcrumbItem = React.forwardRef< + HTMLLIElement, + React.ComponentPropsWithoutRef<"li"> +>(({ className, ...props }, ref) => ( + +)) +BreadcrumbItem.displayName = "BreadcrumbItem" + +const BreadcrumbLink = React.forwardRef< + HTMLAnchorElement, + React.ComponentPropsWithoutRef<"a"> & { + asChild?: boolean + } +>(({ asChild, className, ...props }, ref) => { + const Comp = asChild ? Slot : "a" + + return ( + + ) +}) +BreadcrumbLink.displayName = "BreadcrumbLink" + +const BreadcrumbPage = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef<"span"> +>(({ className, ...props }, ref) => ( + +)) +BreadcrumbPage.displayName = "BreadcrumbPage" + +const BreadcrumbSeparator = ({ + children, + className, + ...props +}: React.ComponentProps<"li">) => ( + svg]:w-3.5 [&>svg]:h-3.5", className)} + {...props} + > + {children ?? } + +) +BreadcrumbSeparator.displayName = "BreadcrumbSeparator" + +const BreadcrumbEllipsis = ({ + className, + ...props +}: React.ComponentProps<"span">) => ( + + + More + +) +BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" + +export { + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbPage, + BreadcrumbSeparator, + BreadcrumbEllipsis, +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 391a566..65d4fcd 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,52 +1,57 @@ -import { cn } from '@/utilities/ui' -import { Slot } from '@radix-ui/react-slot' -import { type VariantProps, cva } from 'class-variance-authority' -import * as React from 'react' +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" const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + "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", { - defaultVariants: { - size: 'default', - variant: 'default', - }, variants: { - size: { - clear: '', - default: 'h-10 px-4 py-2', - icon: 'h-10 w-10', - lg: 'h-11 rounded px-8', - sm: 'h-9 rounded px-3', - }, variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - ghost: 'hover:bg-card hover:text-accent-foreground', - link: 'text-primary items-start justify-start underline-offset-4 hover:underline', - outline: 'border border-border bg-background hover:bg-card hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", }, }, - }, + defaultVariants: { + variant: "default", + size: "default", + }, + } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean - ref?: React.Ref } -const Button: React.FC = ({ - asChild = false, - className, - size, - variant, - ref, - ...props -}) => { - const Comp = asChild ? Slot : 'button' - return -} +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" export { Button, buttonVariants } diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..115cff9 --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,76 @@ +"use client" + +import * as React from "react" +import { ChevronLeft, ChevronRight } from "lucide-react" +import { DayPicker } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +export type CalendarProps = React.ComponentProps + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" + : "[&:has([aria-selected])]:rounded-md" + ), + day: cn( + buttonVariants({ variant: "ghost" }), + "h-8 w-8 p-0 font-normal aria-selected:opacity-100" + ), + day_range_start: "day-range-start", + day_range_end: "day-range-end", + day_selected: + "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", + day_today: "bg-accent text-accent-foreground", + day_outside: + "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", + day_disabled: "text-muted-foreground opacity-50", + day_range_middle: + "aria-selected:bg-accent aria-selected:text-accent-foreground", + day_hidden: "invisible", + ...classNames, + }} + components={{ + IconLeft: ({ className, ...props }) => ( + + ), + IconRight: ({ className, ...props }) => ( + + ), + }} + {...props} + /> + ) +} +Calendar.displayName = "Calendar" + +export { Calendar } diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..9fa4894 --- /dev/null +++ b/src/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..5a20503 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,201 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 10df0d4..69b64fb 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,22 +1,22 @@ -import { cn } from '@/utilities/ui' -import * as React from 'react' +import * as React from "react" -const Input: React.FC< - { - ref?: React.Ref - } & React.InputHTMLAttributes -> = ({ type, className, ref, ...props }) => { - return ( - - ) -} +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" export { Input } diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..70a28f6 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,33 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..12d81c4 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..272cb72 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,140 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Sheet = SheetPrimitive.Root + +const SheetTrigger = SheetPrimitive.Trigger + +const SheetClose = SheetPrimitive.Close + +const SheetPortal = SheetPrimitive.Portal + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +) + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + + + Close + + {children} + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( + +) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( + +) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..91592e6 --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,773 @@ +"use client" + +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { VariantProps, cva } from "class-variance-authority" +import { PanelLeft } from "lucide-react" + +import { useIsMobile } from "@/hooks/use-mobile" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + + + {children} + + + + ) + } +) +SidebarProvider.displayName = "SidebarProvider" + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( + + {children} + + ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + + {children} + + + ) + } + + return ( + + {/* This is what handles the sidebar gap on desktop */} + + + + {children} + + + + ) + } +) +Sidebar.displayName = "Sidebar" + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + { + onClick?.(event) + toggleSidebar() + }} + {...props} + > + + Toggle Sidebar + + ) +}) +SidebarTrigger.displayName = "SidebarTrigger" + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + + ) +}) +SidebarRail.displayName = "SidebarRail" + +const SidebarInset = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"main"> +>(({ className, ...props }, ref) => { + return ( + + ) +}) +SidebarInset.displayName = "SidebarInset" + +const SidebarInput = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) +SidebarInput.displayName = "SidebarInput" + +const SidebarHeader = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + + ) +}) +SidebarHeader.displayName = "SidebarHeader" + +const SidebarFooter = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + + ) +}) +SidebarFooter.displayName = "SidebarFooter" + +const SidebarSeparator = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) +SidebarSeparator.displayName = "SidebarSeparator" + +const SidebarContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + + ) +}) +SidebarContent.displayName = "SidebarContent" + +const SidebarGroup = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => { + return ( + + ) +}) +SidebarGroup.displayName = "SidebarGroup" + +const SidebarGroupLabel = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { asChild?: boolean } +>(({ className, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "div" + + return ( + svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + className + )} + {...props} + /> + ) +}) +SidebarGroupLabel.displayName = "SidebarGroupLabel" + +const SidebarGroupAction = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { asChild?: boolean } +>(({ className, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 after:md:hidden", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +}) +SidebarGroupAction.displayName = "SidebarGroupAction" + +const SidebarGroupContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => ( + +)) +SidebarGroupContent.displayName = "SidebarGroupContent" + +const SidebarMenu = React.forwardRef< + HTMLUListElement, + React.ComponentProps<"ul"> +>(({ className, ...props }, ref) => ( + +)) +SidebarMenu.displayName = "SidebarMenu" + +const SidebarMenuItem = React.forwardRef< + HTMLLIElement, + React.ComponentProps<"li"> +>(({ className, ...props }, ref) => ( + +)) +SidebarMenuItem.displayName = "SidebarMenuItem" + +const sidebarMenuButtonVariants = cva( + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + outline: + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + }, + size: { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +const SidebarMenuButton = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { + asChild?: boolean + isActive?: boolean + tooltip?: string | React.ComponentProps + } & VariantProps +>( + ( + { + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props + }, + ref + ) => { + const Comp = asChild ? Slot : "button" + const { isMobile, state } = useSidebar() + + const button = ( + + ) + + if (!tooltip) { + return button + } + + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + } + } + + return ( + + {button} + + + ) + } +) +SidebarMenuButton.displayName = "SidebarMenuButton" + +const SidebarMenuAction = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & { + asChild?: boolean + showOnHover?: boolean + } +>(({ className, asChild = false, showOnHover = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 after:md:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + showOnHover && + "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", + className + )} + {...props} + /> + ) +}) +SidebarMenuAction.displayName = "SidebarMenuAction" + +const SidebarMenuBadge = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> +>(({ className, ...props }, ref) => ( + +)) +SidebarMenuBadge.displayName = "SidebarMenuBadge" + +const SidebarMenuSkeleton = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + showIcon?: boolean + } +>(({ className, showIcon = false, ...props }, ref) => { + // Random width between 50 to 90%. + const width = React.useMemo(() => { + return `${Math.floor(Math.random() * 40) + 50}%` + }, []) + + return ( + + {showIcon && ( + + )} + + + ) +}) +SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton" + +const SidebarMenuSub = React.forwardRef< + HTMLUListElement, + React.ComponentProps<"ul"> +>(({ className, ...props }, ref) => ( + +)) +SidebarMenuSub.displayName = "SidebarMenuSub" + +const SidebarMenuSubItem = React.forwardRef< + HTMLLIElement, + React.ComponentProps<"li"> +>(({ ...props }, ref) => ) +SidebarMenuSubItem.displayName = "SidebarMenuSubItem" + +const SidebarMenuSubButton = React.forwardRef< + HTMLAnchorElement, + React.ComponentProps<"a"> & { + asChild?: boolean + size?: "sm" | "md" + isActive?: boolean + } +>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { + const Comp = asChild ? Slot : "a" + + return ( + span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", + size === "sm" && "text-xs", + size === "md" && "text-sm", + "group-data-[collapsible=icon]:hidden", + className + )} + {...props} + /> + ) +}) +SidebarMenuSubButton.displayName = "SidebarMenuSubButton" + +export { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSkeleton, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarSeparator, + SidebarTrigger, + useSidebar, +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..d7e45f7 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( + + ) +} + +export { Skeleton } diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..28e1918 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/hooks/use-mobile.tsx b/src/hooks/use-mobile.tsx new file mode 100644 index 0000000..2b0fe1d --- /dev/null +++ b/src/hooks/use-mobile.tsx @@ -0,0 +1,19 @@ +import * as React from "react" + +const MOBILE_BREAKPOINT = 768 + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState(undefined) + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + } + mql.addEventListener("change", onChange) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + return () => mql.removeEventListener("change", onChange) + }, []) + + return !!isMobile +} diff --git a/src/lib/utils/index.tsx b/src/lib/utils/index.tsx new file mode 100644 index 0000000..fc08f6e --- /dev/null +++ b/src/lib/utils/index.tsx @@ -0,0 +1,12 @@ +/** + * Utility functions for UI components automatically added by ShadCN and used in a few of our frontend components and blocks. + * + * Other functions may be exported from here in the future or by installing other shadcn components. + */ + +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/tailwind.config.mjs b/tailwind.config.mjs index 44469ab..3bf3331 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -9,7 +9,7 @@ const config = { './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', ], - darkMode: ['selector', '[data-theme="dark"]'], + darkMode: ['selector', '[data-theme="dark"]', 'class'], plugins: [tailwindcssAnimate, typography], prefix: '', safelist: [ @@ -27,126 +27,109 @@ const config = { 'bg-warning/30', ], theme: { - container: { - center: true, - padding: { - '2xl': '2rem', - DEFAULT: '1rem', - lg: '2rem', - md: '2rem', - sm: '1rem', - xl: '2rem', - }, - screens: { - '2xl': '86rem', - lg: '64rem', - md: '48rem', - sm: '40rem', - xl: '80rem', - }, - }, - extend: { - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - colors: { - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - background: 'hsl(var(--background))', - border: 'hsla(var(--border))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - foreground: 'hsl(var(--foreground))', - input: 'hsl(var(--input))', - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - ring: 'hsl(var(--ring))', - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - success: 'hsl(var(--success))', - error: 'hsl(var(--error))', - warning: 'hsl(var(--warning))', - }, - fontFamily: { - mono: ['var(--font-geist-mono)'], - sans: ['var(--font-geist-sans)'], - }, - keyframes: { - 'accordion-down': { - from: { height: '0' }, - to: { height: 'var(--radix-accordion-content-height)' }, - }, - 'accordion-up': { - from: { height: 'var(--radix-accordion-content-height)' }, - to: { height: '0' }, - }, - }, - typography: () => ({ - DEFAULT: { - css: [ - { - '--tw-prose-body': 'var(--text)', - '--tw-prose-headings': 'var(--text)', - h1: { - fontWeight: 'normal', - marginBottom: '0.25em', - }, - }, - ], - }, - base: { - css: [ - { - h1: { - fontSize: '2.5rem', - }, - h2: { - fontSize: '1.25rem', - fontWeight: 600, - }, - }, - ], - }, - md: { - css: [ - { - h1: { - fontSize: '3.5rem', - }, - h2: { - fontSize: '1.5rem', - }, - }, - ], - }, - }), - }, + container: { + center: true, + padding: { + '2xl': '2rem', + DEFAULT: '1rem', + lg: '2rem', + md: '2rem', + sm: '1rem', + xl: '2rem' + }, + screens: { + '2xl': '86rem', + lg: '64rem', + md: '48rem', + sm: '40rem', + xl: '80rem' + } + }, + extend: { + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + background: 'hsl(var(--background))', + border: 'hsla(var(--border))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + foreground: 'hsl(var(--foreground))', + input: 'hsl(var(--input))', + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + ring: 'hsl(var(--ring))', + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + success: 'hsl(var(--success))', + error: 'hsl(var(--error))', + warning: 'hsl(var(--warning))', + sidebar: { + DEFAULT: 'hsl(var(--sidebar-background))', + foreground: 'hsl(var(--sidebar-foreground))', + primary: 'hsl(var(--sidebar-primary))', + 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', + accent: 'hsl(var(--sidebar-accent))', + 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', + border: 'hsl(var(--sidebar-border))', + ring: 'hsl(var(--sidebar-ring))' + } + }, + fontFamily: { + mono: [ + 'var(--font-geist-mono)' + ], + sans: [ + 'var(--font-geist-sans)' + ] + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + } + } + } }, }