diff --git a/fix-import-aliases.sh b/bash-setup-scripts/fix-import-aliases.sh
similarity index 100%
rename from fix-import-aliases.sh
rename to bash-setup-scripts/fix-import-aliases.sh
diff --git a/generate_admin_ui.sh b/bash-setup-scripts/generate_admin_ui.sh
similarity index 100%
rename from generate_admin_ui.sh
rename to bash-setup-scripts/generate_admin_ui.sh
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..823a965
--- /dev/null
+++ b/components.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": false,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/app/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/package-lock.json b/package-lock.json
index 9f1d55e..e202bbc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,12 +9,22 @@
"version": "0.1.0",
"license": "ISC",
"dependencies": {
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
"@supabase/supabase-js": "^2.84.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
"fs": "^0.0.1-security",
+ "lucide-react": "^0.554.0",
"next": "16.0.3",
"path": "^0.12.7",
"react": "19.2.0",
- "react-dom": "19.2.0"
+ "react-dom": "19.2.0",
+ "tailwind-merge": "^3.4.0",
+ "tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -426,6 +436,40 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1459,6 +1503,690 @@
"node": ">=0.10"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "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-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@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-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "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-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "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-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "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-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "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-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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-label": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
+ "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "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.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "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.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "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-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@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-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.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-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==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
+ "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "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-separator/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "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.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "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-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "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",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "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-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "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-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "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-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2444,6 +3172,17 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -2866,11 +3605,30 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3038,6 +3796,11 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3909,6 +4672,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -5008,6 +5779,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.554.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz",
+ "integrity": "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5579,6 +6358,72 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -6187,11 +7032,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
+ "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
- "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
- "dev": true
+ "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -6523,6 +7384,47 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
diff --git a/package.json b/package.json
index 2e23bf7..d85822a 100644
--- a/package.json
+++ b/package.json
@@ -9,12 +9,22 @@
"lint": "eslint"
},
"dependencies": {
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
"@supabase/supabase-js": "^2.84.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
"fs": "^0.0.1-security",
+ "lucide-react": "^0.554.0",
"next": "16.0.3",
"path": "^0.12.7",
"react": "19.2.0",
- "react-dom": "19.2.0"
+ "react-dom": "19.2.0",
+ "tailwind-merge": "^3.4.0",
+ "tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
diff --git a/src/app/admin/components/AdminHeader.js b/src/app/admin/components/AdminHeader.js
new file mode 100644
index 0000000..a978cf4
--- /dev/null
+++ b/src/app/admin/components/AdminHeader.js
@@ -0,0 +1,19 @@
+"use client";
+
+import { usePathname } from "next/navigation";
+
+export default function AdminHeader() {
+ const pathname = usePathname();
+
+ // Convert pathname into readable title:
+ const title =
+ pathname.replace("/admin", "").replace("/", " › ").trim() || "Dashboard";
+
+ return (
+
+
+ {title === "" ? "Dashboard" : title}
+
+
+ );
+}
diff --git a/src/app/admin/components/AdminSidebar.js b/src/app/admin/components/AdminSidebar.js
new file mode 100644
index 0000000..57efcfb
--- /dev/null
+++ b/src/app/admin/components/AdminSidebar.js
@@ -0,0 +1,48 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { cn } from "@/lib/utils";
+
+export default function AdminSidebar() {
+ const pathname = usePathname();
+
+ const nav = [
+ { href: "/admin", label: "Dashboard" },
+ { href: "/admin/models", label: "Models" },
+ { href: "/admin/parts", label: "Parts" },
+ ];
+
+ return (
+
+ );
+}
diff --git a/src/app/admin/layout.js b/src/app/admin/layout.js
index 9793d25..1dee319 100644
--- a/src/app/admin/layout.js
+++ b/src/app/admin/layout.js
@@ -1,33 +1,20 @@
-import React from "react";
-import Link from "next/link";
-import "../globals.css";
+import AdminSidebar from "./components/AdminSidebar";
+import AdminHeader from "./components/AdminHeader";
export default function AdminLayout({ children }) {
return (
-
+ {/* MAIN COLUMN */}
+
+ {/* TOP HEADER */}
+
-
{children}
+ {/* PAGE CONTENT */}
+
{children}
+
);
}
diff --git a/src/app/admin/models/[id]/add-part/page.js b/src/app/admin/models/[id]/add-part/page.js
new file mode 100644
index 0000000..9c90c1d
--- /dev/null
+++ b/src/app/admin/models/[id]/add-part/page.js
@@ -0,0 +1,167 @@
+"use client";
+
+import { use, useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { supabaseBrowser } from "@/lib/supabaseClient";
+
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Select,
+ SelectTrigger,
+ SelectValue,
+ SelectContent,
+ SelectItem,
+} from "@/components/ui/select";
+
+export default function AddPartToModelPage({ params }) {
+ const { id: modelId } = use(params); // React 19 unwrap
+ const router = useRouter();
+
+ const [search, setSearch] = useState("");
+ const [results, setResults] = useState([]);
+ const [selectedPart, setSelectedPart] = useState(null);
+
+ const [fitType, setFitType] = useState("direct");
+ const [notes, setNotes] = useState("");
+
+ // Search parts live
+ useEffect(() => {
+ async function load() {
+ if (!search.trim()) {
+ setResults([]);
+ return;
+ }
+
+ const { data } = await supabaseBrowser
+ .from("parts")
+ .select("*")
+ .ilike("name", `%${search}%`)
+ .limit(20);
+
+ setResults(data || []);
+ }
+
+ load();
+ }, [search]);
+
+ async function save() {
+ if (!selectedPart) return;
+
+ await supabaseBrowser.from("parts_compatible_models").insert({
+ part_id: selectedPart.id,
+ model_id: modelId,
+ fit_type: fitType,
+ notes: notes || null,
+ });
+
+ router.push(`/admin/models/${modelId}`);
+ }
+
+ return (
+
+
+ Add Compatible Part
+
+
+
+ {/* SEARCH */}
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ {/* SEARCH RESULTS */}
+ {!selectedPart && results.length > 0 && (
+
+ {results.map((p) => (
+
+ ))}
+
+ )}
+
+ {/* SELECTED PART CARD */}
+ {selectedPart && (
+
+
{selectedPart.name}
+
+ {selectedPart.manufacturer || "-"} –{" "}
+ {selectedPart.category || "-"}
+
+
+
+
+ )}
+
+ {/* FIT TYPE + NOTES */}
+ {selectedPart && (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* SAVE */}
+
+ >
+ )}
+
+
+
+
+ );
+}
diff --git a/src/app/admin/models/[id]/page.js b/src/app/admin/models/[id]/page.js
index e11ab34..d21d6b4 100644
--- a/src/app/admin/models/[id]/page.js
+++ b/src/app/admin/models/[id]/page.js
@@ -1,55 +1,299 @@
+"use client";
+
+import { use, useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
import Link from "next/link";
-import { supabaseServer } from "@/lib/supabaseServer";
+import { supabaseBrowser } from "@/lib/supabaseClient";
-export default async function ModelDetail({ params }) {
- const { id } = params;
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import { Badge } from "@/components/ui/badge";
+import { Separator } from "@/components/ui/separator";
- const { data: model } = await supabaseServer
- .from("models")
- .select("*")
- .eq("id", id)
- .single();
+import {
+ Dialog,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from "@/components/ui/dialog";
- const { data: compat } = await supabaseServer
- .from("parts_compatible_models")
- .select("*, parts(*)")
- .eq("model_id", id);
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+export default function ModelDetailPage({ params }) {
+ const { id } = use(params); // React 19: unwrap params
+ const router = useRouter();
+
+ const [model, setModel] = useState(null);
+ const [compat, setCompat] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [deleteOpen, setDeleteOpen] = useState(false);
+
+ // Load model + compatibility
+ useEffect(() => {
+ async function load() {
+ setLoading(true);
+
+ const { data: modelData } = await supabaseBrowser
+ .from("models")
+ .select("*")
+ .eq("id", id)
+ .single();
+
+ setModel(modelData || null);
+
+ const { data: compatData } = await supabaseBrowser
+ .from("parts_compatible_models")
+ .select("*, parts(*)")
+ .eq("model_id", id);
+
+ setCompat(compatData || []);
+ setLoading(false);
+ }
+
+ load();
+ }, [id]);
+
+ // Save model changes
+ async function saveModel() {
+ if (!model) return;
+ setSaving(true);
+
+ let metadataObj = null;
+
+ if (typeof model.metadata === "string" && model.metadata.trim() !== "") {
+ try {
+ metadataObj = JSON.parse(model.metadata);
+ } catch {
+ alert("Metadata must be valid JSON.");
+ setSaving(false);
+ return;
+ }
+ }
+
+ const updatePayload = {
+ name: model.name,
+ category: model.category || null,
+ manufacturer: model.manufacturer || null,
+ year: model.year || null,
+ description: model.description || null,
+ metadata: metadataObj,
+ };
+
+ await supabaseBrowser.from("models").update(updatePayload).eq("id", id);
+ setSaving(false);
+ }
+
+ async function deleteModel() {
+ await supabaseBrowser.from("models").delete().eq("id", id);
+ router.push("/admin/models");
+ }
+
+ async function removeCompatibility(compatId) {
+ await supabaseBrowser
+ .from("parts_compatible_models")
+ .delete()
+ .eq("id", compatId);
+
+ setCompat((prev) => prev.filter((c) => c.id !== compatId));
+ }
+
+ // PREVENT NULL CRASHES
+ if (loading) return Loading model…
;
+ if (!model) return Model not found.
;
return (
-
-
{model?.name}
+
+ {/* MODEL EDIT CARD */}
+
+
+ Edit Model
+
-
-
Brand: {model?.brand}
-
Scale: {model?.scale}
-
Chassis: {model?.chassis_type}
-
-
- Compatible Parts
-
-
- + Add Compatible Part
-
-
-
- {compat?.map((c) => (
-
-
-
{c.parts.name}
-
{c.fit_type}
-
-
- View Part
-
+
+ {/* NAME */}
+
+
+ setModel({ ...model, name: e.target.value })}
+ />
- ))}
-
+
+ {/* GRID FIELDS */}
+
+
+ {/* DESCRIPTION */}
+
+
+
+
+ {/* METADATA */}
+
+
+
+
+ {/* ACTION BUTTONS */}
+
+
+
+ {/* DELETE WITH DIALOG */}
+
+
+
+
+
+ {/* COMPATIBILITY TABLE */}
+
+
+ Compatible Parts
+
+
+
+
+
+
+
+
+
+
+ Part
+ Fit Type
+ Notes
+ Actions
+
+
+
+
+ {compat.map((c) => (
+
+ {c.parts?.name}
+
+
+
+ {c.fit_type?.replace("_", " ")}
+
+
+
+ {c.notes || "-"}
+
+
+
+
+
+ ))}
+
+ {compat.length === 0 && (
+
+
+ No compatible parts found.
+
+
+ )}
+
+
+
+
);
}
diff --git a/src/app/admin/models/create/page.js b/src/app/admin/models/create/page.js
new file mode 100644
index 0000000..8678299
--- /dev/null
+++ b/src/app/admin/models/create/page.js
@@ -0,0 +1,126 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { supabaseBrowser } from "@/lib/supabaseClient";
+
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+
+export default function CreateModelPage() {
+ const router = useRouter();
+
+ const [form, setForm] = useState({
+ name: "",
+ manufacturer: "",
+ category: "",
+ year: "",
+ description: "",
+ metadata: "",
+ });
+
+ async function save() {
+ let metadataObj = null;
+
+ if (form.metadata.trim()) {
+ try {
+ metadataObj = JSON.parse(form.metadata);
+ } catch {
+ alert("Metadata must be valid JSON");
+ return;
+ }
+ }
+
+ const { error } = await supabaseBrowser.from("models").insert({
+ name: form.name,
+ manufacturer: form.manufacturer || null,
+ category: form.category || null,
+ year: form.year || null,
+ description: form.description || null,
+ metadata: metadataObj,
+ });
+
+ if (!error) {
+ router.push("/admin/models");
+ } else {
+ alert(error.message);
+ }
+ }
+
+ return (
+
+
+ Create New Model
+
+
+
+
+
+ setForm({ ...form, name: e.target.value })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/models/new/page.js b/src/app/admin/models/new/page.js
deleted file mode 100644
index 6982cd8..0000000
--- a/src/app/admin/models/new/page.js
+++ /dev/null
@@ -1,48 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { supabaseBrowser } from "@/lib/supabaseClient";
-import { useRouter } from "next/navigation";
-
-export default function NewModelPage() {
- const [form, setForm] = useState({
- name: "",
- brand: "",
- scale: "",
- chassis_type: "",
- notes: "",
- });
-
- const router = useRouter();
-
- async function save() {
- const { error } = await supabaseBrowser.from("models").insert(form);
- if (!error) router.push("/admin/models");
- }
-
- return (
-
-
Add New Model
-
-
- {Object.entries(form).map(([key, value]) => (
-
-
- setForm({ ...form, [key]: e.target.value })}
- />
-
- ))}
-
-
-
-
- );
-}
diff --git a/src/app/admin/models/page.js b/src/app/admin/models/page.js
index 4f963f4..5c2016f 100644
--- a/src/app/admin/models/page.js
+++ b/src/app/admin/models/page.js
@@ -1,43 +1,211 @@
-import Link from "next/link";
-import { supabaseServer } from "@/lib/supabaseServer";
+"use client";
-export default async function ModelsPage() {
- const { data: models } = await supabaseServer
- .from("models")
- .select("*")
- .order("name");
+import { useEffect, useState, useMemo } from "react";
+import Link from "next/link";
+import { supabaseBrowser } from "@/lib/supabaseClient";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectTrigger,
+ SelectContent,
+ SelectItem,
+ SelectValue,
+} from "@/components/ui/select";
+
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import {
+ Table,
+ TableHead,
+ TableRow,
+ TableHeader,
+ TableBody,
+ TableCell,
+} from "@/components/ui/table";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function ModelsListPage() {
+ const [models, setModels] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Search / Filter / Sort states
+ const [search, setSearch] = useState("");
+ const [manufacturerFilter, setManufacturerFilter] = useState("all");
+ const [sort, setSort] = useState("asc"); // asc | desc | newest | oldest
+
+ useEffect(() => {
+ async function load() {
+ setLoading(true);
+
+ const { data } = await supabaseBrowser
+ .from("models")
+ .select("*")
+ .order("name", { ascending: true });
+
+ setModels(data || []);
+ setLoading(false);
+ }
+
+ load();
+ }, []);
+
+ // ----------------------------------------------
+ // 🎯 FILTERING + SEARCH + SORTING (client-side)
+ // ----------------------------------------------
+ const filteredModels = useMemo(() => {
+ let list = [...models];
+
+ // SEARCH
+ if (search.trim()) {
+ list = list.filter((m) =>
+ m.name.toLowerCase().includes(search.toLowerCase())
+ );
+ }
+
+ // MANUFACTURER FILTER
+ if (manufacturerFilter !== "all") {
+ list = list.filter((m) => m.manufacturer === manufacturerFilter);
+ }
+
+ // SORTING
+ switch (sort) {
+ case "asc":
+ list.sort((a, b) => a.name.localeCompare(b.name));
+ break;
+ case "desc":
+ list.sort((a, b) => b.name.localeCompare(a.name));
+ break;
+ case "newest":
+ list.sort((a, b) => (b.year || 0) - (a.year || 0));
+ break;
+ case "oldest":
+ list.sort((a, b) => (a.year || 0) - (b.year || 0));
+ break;
+ }
+
+ return list;
+ }, [models, search, manufacturerFilter, sort]);
+
+ // SKELETON
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ // Collect manufacturers for filter dropdown
+ const manufacturers = Array.from(
+ new Set(models.map((m) => m.manufacturer).filter(Boolean))
+ );
return (
-
-
-
Models
-
- + Add Model
-
-
+
+
+ Models
- {(!models || models.length === 0) && (
-
- No models found yet. Click “Add Model” to create your
- first one.
-
- )}
+
+
-
- {models?.map((m) => (
-
+ {/* SEARCH + FILTERS + SORT BAR */}
+
+ {/* SEARCH */}
+
setSearch(e.target.value)}
+ className="md:w-1/3"
+ />
+
+ {/* MANUFACTURER FILTER */}
+
-
+
+
+
+
+
+ All Manufacturers
+ {manufacturers.map((m) => (
+
+ {m}
+
+ ))}
+
+
+
+ {/* SORTING */}
+
+
+
+ {/* TABLE */}
+
+
+
+ Name
+ Manufacturer
+ Category
+ Year
+ Actions
+
+
+
+
+ {filteredModels.map((m) => (
+
+ {m.name}
+ {m.manufacturer || "-"}
+ {m.category || "-"}
+ {m.year || "-"}
+
+
+
+
+
+ ))}
+
+ {filteredModels.length === 0 && (
+
+
+ No models found.
+
+
+ )}
+
+
+
+
);
}
diff --git a/src/app/admin/parts/[id]/add-model/page.js b/src/app/admin/parts/[id]/add-model/page.js
new file mode 100644
index 0000000..810ab25
--- /dev/null
+++ b/src/app/admin/parts/[id]/add-model/page.js
@@ -0,0 +1,170 @@
+"use client";
+
+import { use, useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { supabaseBrowser } from "@/lib/supabaseClient";
+
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Select,
+ SelectTrigger,
+ SelectValue,
+ SelectContent,
+ SelectItem,
+} from "@/components/ui/select";
+
+export default function AddModelToPartPage({ params }) {
+ const { id: partId } = use(params); // React 19 unwrapping
+ const router = useRouter();
+
+ const [search, setSearch] = useState("");
+ const [results, setResults] = useState([]);
+ const [selectedModel, setSelectedModel] = useState(null);
+
+ const [fitType, setFitType] = useState("direct");
+ const [notes, setNotes] = useState("");
+
+ // 🔍 Live search for models
+ useEffect(() => {
+ async function load() {
+ if (!search.trim()) {
+ setResults([]);
+ return;
+ }
+
+ const { data } = await supabaseBrowser
+ .from("models")
+ .select("*")
+ .ilike("name", `%${search}%`)
+ .limit(20);
+
+ setResults(data || []);
+ }
+
+ load();
+ }, [search]);
+
+ // 💾 Save compatibility entry
+ async function save() {
+ if (!selectedModel) return;
+
+ await supabaseBrowser.from("parts_compatible_models").insert({
+ part_id: partId,
+ model_id: selectedModel.id,
+ fit_type: fitType,
+ notes: notes || null,
+ });
+
+ router.push(`/admin/parts/${partId}`);
+ }
+
+ return (
+
+
+ Add Compatible Model
+
+
+
+ {/* SEARCH FIELD */}
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ {/* SEARCH RESULTS */}
+ {!selectedModel && results.length > 0 && (
+
+ {results.map((m) => (
+
+ ))}
+
+ )}
+
+ {/* SELECTED MODEL CARD */}
+ {selectedModel && (
+
+
{selectedModel.name}
+
+ {selectedModel.manufacturer || "-"} —{" "}
+ {selectedModel.category || "-"}
+
+
+
+
+ )}
+
+ {/* FIT-TYPE + NOTES */}
+ {selectedModel && (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* SAVE BUTTON */}
+
+ >
+ )}
+
+ {/* CANCEL BUTTON */}
+
+
+
+ );
+}
diff --git a/src/app/admin/parts/[id]/page.js b/src/app/admin/parts/[id]/page.js
index fa115af..c4f2639 100644
--- a/src/app/admin/parts/[id]/page.js
+++ b/src/app/admin/parts/[id]/page.js
@@ -1,221 +1,292 @@
"use client";
-import { useEffect, useState } from "react";
+import { use, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
+import Link from "next/link";
import { supabaseBrowser } from "@/lib/supabaseClient";
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
+import { Separator } from "@/components/ui/separator";
+
+import {
+ Dialog,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from "@/components/ui/dialog";
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
export default function PartDetailPage({ params }) {
- const { id } = params;
+ const { id } = use(params); // React 19 unwrap
const router = useRouter();
+ const [part, setPart] = useState(null);
+ const [compat, setCompat] = useState([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
- const [deleting, setDeleting] = useState(false);
- const [errorMsg, setErrorMsg] = useState("");
- const [form, setForm] = useState({
- name: "",
- manufacturer: "",
- sku: "",
- category: "",
- description: "",
- metadata: "",
- });
+ const [deleteOpen, setDeleteOpen] = useState(false);
+ // Load part data + compatibility
useEffect(() => {
- async function fetchPart() {
+ async function load() {
setLoading(true);
- setErrorMsg("");
- const { data, error } = await supabaseBrowser
+ const { data: partData } = await supabaseBrowser
.from("parts")
.select("*")
.eq("id", id)
.single();
- if (error) {
- console.error(error);
- setErrorMsg("Failed to load part.");
- setLoading(false);
- return;
- }
+ setPart(partData || null);
- setForm({
- name: data.name || "",
- manufacturer: data.manufacturer || "",
- sku: data.sku || "",
- category: data.category || "",
- description: data.description || "",
- metadata: data.metadata ? JSON.stringify(data.metadata, null, 2) : "",
- });
+ const { data: compatData } = await supabaseBrowser
+ .from("parts_compatible_models")
+ .select("*, models(*)")
+ .eq("part_id", id);
+ setCompat(compatData || []);
setLoading(false);
}
- fetchPart();
+ load();
}, [id]);
- function updateField(field, value) {
- setForm((prev) => ({ ...prev, [field]: value }));
- }
-
- async function handleSave() {
+ // Save part edits
+ async function savePart() {
+ if (!part) return;
setSaving(true);
- setErrorMsg("");
- let metadata = null;
- if (form.metadata.trim()) {
+ let metadataObj = null;
+
+ if (typeof part.metadata === "string" && part.metadata.trim() !== "") {
try {
- metadata = JSON.parse(form.metadata);
- } catch (err) {
+ metadataObj = JSON.parse(part.metadata);
+ } catch {
+ alert("Metadata must be valid JSON.");
setSaving(false);
- setErrorMsg("Metadata must be valid JSON.");
return;
}
}
- const payload = {
- name: form.name.trim(),
- manufacturer: form.manufacturer || null,
- sku: form.sku || null,
- category: form.category || null,
- description: form.description || null,
- metadata,
+ const updatePayload = {
+ name: part.name,
+ manufacturer: part.manufacturer || null,
+ sku: part.sku || null,
+ category: part.category || null,
+ description: part.description || null,
+ metadata: metadataObj,
};
- const { error } = await supabaseBrowser
- .from("parts")
- .update(payload)
- .eq("id", id);
-
- if (error) {
- console.error(error);
- setErrorMsg(error.message || "Failed to save changes.");
- setSaving(false);
- return;
- }
-
+ await supabaseBrowser.from("parts").update(updatePayload).eq("id", id);
setSaving(false);
}
- async function handleDelete() {
- const ok = window.confirm(
- "Are you sure you want to delete this part? This cannot be undone."
- );
- if (!ok) return;
-
- setDeleting(true);
- setErrorMsg("");
-
- const { error } = await supabaseBrowser.from("parts").delete().eq("id", id);
-
- if (error) {
- console.error(error);
- setErrorMsg(error.message || "Failed to delete part.");
- setDeleting(false);
- return;
- }
-
+ async function deletePart() {
+ await supabaseBrowser.from("parts").delete().eq("id", id);
router.push("/admin/parts");
}
- if (loading) {
- return
Loading part...
;
+ async function removeCompatibility(compatId) {
+ await supabaseBrowser
+ .from("parts_compatible_models")
+ .delete()
+ .eq("id", compatId);
+
+ setCompat((prev) => prev.filter((c) => c.id !== compatId));
}
+ // Prevent render crashes
+ if (loading) return
Loading part…
;
+ if (!part) return
Part not found.
;
+
return (
-
-
Edit Part
+
+ {/* PART EDIT FORM */}
+
+
+ Edit Part
+
-
-
-
- updateField("name", e.target.value)}
- />
-
+
+
+
+ setPart({ ...part, name: e.target.value })}
+ />
+
+
+
-
-
- updateField("manufacturer", e.target.value)}
+
+
-
- updateField("sku", e.target.value)}
+
+
-
-
-
updateField("category", e.target.value)}
- />
+ {/* SAVE + DELETE */}
+
+
+
+
-
+
+
-
-
-
+ {/* COMPATIBLE MODELS TABLE */}
+
+
+ Compatible Models
-
-
-
+
+
- {errorMsg && {errorMsg}
}
+
-
-
+
+
+
+
+ Model
+ Fit Type
+ Notes
+ Actions
+
+
-
+
+ {compat.map((c) => (
+
+ {c.models?.name}
-
-
-
+
+
+ {c.fit_type?.replace("_", " ")}
+
+
+
+ {c.notes || "-"}
+
+
+
+
+
+ ))}
+
+ {compat.length === 0 && (
+
+
+ No models linked to this part.
+
+
+ )}
+
+
+
+
);
}
diff --git a/src/app/admin/parts/create/page.js b/src/app/admin/parts/create/page.js
new file mode 100644
index 0000000..1954149
--- /dev/null
+++ b/src/app/admin/parts/create/page.js
@@ -0,0 +1,149 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { supabaseBrowser } from "@/lib/supabaseClient";
+
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+
+export default function CreatePartPage() {
+ const router = useRouter();
+
+ const [form, setForm] = useState({
+ name: "",
+ manufacturer: "",
+ category: "",
+ sku: "",
+ description: "",
+ metadata: "",
+ });
+
+ const [saving, setSaving] = useState(false);
+
+ async function save() {
+ setSaving(true);
+
+ let metadataObj = null;
+
+ // Parse metadata if provided
+ if (form.metadata.trim()) {
+ try {
+ metadataObj = JSON.parse(form.metadata);
+ } catch {
+ alert("Metadata must be valid JSON.");
+ setSaving(false);
+ return;
+ }
+ }
+
+ // Insert into Supabase
+ const { error } = await supabaseBrowser.from("parts").insert({
+ name: form.name,
+ manufacturer: form.manufacturer || null,
+ category: form.category || null,
+ sku: form.sku || null,
+ description: form.description || null,
+ metadata: metadataObj,
+ });
+
+ setSaving(false);
+
+ if (!error) {
+ router.push("/admin/parts");
+ } else {
+ alert(error.message);
+ }
+ }
+
+ return (
+
+
+ Create New Part
+
+
+
+ {/* NAME */}
+
+
+ setForm({ ...form, name: e.target.value })}
+ placeholder="e.g., Tamiya Hotshot Gearbox"
+ />
+
+
+ {/* GRID FIELDS */}
+
+
+ {/* DESCRIPTION */}
+
+
+
+
+ {/* METADATA */}
+
+
+
+
+ {/* ACTION BUTTONS */}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/parts/new/page.js b/src/app/admin/parts/new/page.js
deleted file mode 100644
index fb17874..0000000
--- a/src/app/admin/parts/new/page.js
+++ /dev/null
@@ -1,151 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { supabaseBrowser } from "@/lib/supabaseClient";
-
-export default function NewPartPage() {
- const [form, setForm] = useState({
- name: "",
- manufacturer: "",
- sku: "",
- category: "",
- description: "",
- metadata: "", // JSON as text
- });
-
- const [saving, setSaving] = useState(false);
- const [errorMsg, setErrorMsg] = useState("");
- const router = useRouter();
-
- function updateField(field, value) {
- setForm((prev) => ({ ...prev, [field]: value }));
- }
-
- async function handleSave() {
- setSaving(true);
- setErrorMsg("");
-
- let metadata = null;
- if (form.metadata.trim()) {
- try {
- metadata = JSON.parse(form.metadata);
- } catch (err) {
- setSaving(false);
- setErrorMsg("Metadata must be valid JSON.");
- return;
- }
- }
-
- const payload = {
- name: form.name.trim(),
- manufacturer: form.manufacturer || null,
- sku: form.sku || null,
- category: form.category || null,
- description: form.description || null,
- metadata,
- };
-
- const { error } = await supabaseBrowser.from("parts").insert(payload);
-
- if (error) {
- console.error(error);
- setErrorMsg(error.message || "Failed to save part.");
- setSaving(false);
- return;
- }
-
- router.push("/admin/parts");
- }
-
- return (
-
-
Add New Part
-
-
-
-
- updateField("name", e.target.value)}
- placeholder="e.g. Alloy Suspension Arm Set"
- />
-
-
-
-
-
-
-
-
-
-
-
-
- {errorMsg &&
{errorMsg}
}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/admin/parts/page.js b/src/app/admin/parts/page.js
index a2240a7..d0fb094 100644
--- a/src/app/admin/parts/page.js
+++ b/src/app/admin/parts/page.js
@@ -1,60 +1,236 @@
+"use client";
+
+import { useEffect, useState, useMemo } from "react";
import Link from "next/link";
-import { supabaseServer } from "@/lib/supabaseServer";
+import { supabaseBrowser } from "@/lib/supabaseClient";
-export default async function PartsPage() {
- const { data: parts, error } = await supabaseServer
- .from("parts")
- .select("*")
- .order("name", { ascending: true });
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
- console.log("here");
+import {
+ Select,
+ SelectTrigger,
+ SelectContent,
+ SelectItem,
+ SelectValue,
+} from "@/components/ui/select";
- if (error) {
- console.error("Error loading parts:", error);
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+
+import {
+ Table,
+ TableHead,
+ TableRow,
+ TableHeader,
+ TableBody,
+ TableCell,
+} from "@/components/ui/table";
+
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function PartsListPage() {
+ const [parts, setParts] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // UI state: Search, filters, sorting
+ const [search, setSearch] = useState("");
+ const [manufacturerFilter, setManufacturerFilter] = useState("all");
+ const [categoryFilter, setCategoryFilter] = useState("all");
+ const [sort, setSort] = useState("asc"); // asc | desc
+
+ useEffect(() => {
+ async function load() {
+ setLoading(true);
+
+ const { data } = await supabaseBrowser
+ .from("parts")
+ .select("*")
+ .order("name", { ascending: true });
+
+ setParts(data || []);
+ setLoading(false);
+ }
+
+ load();
+ }, []);
+
+ // --------------------------------------------------------
+ // 🎯 FILTER + SEARCH + SORT (client-side, fast)
+ // --------------------------------------------------------
+ const filteredParts = useMemo(() => {
+ let list = [...parts];
+
+ // SEARCH
+ if (search.trim()) {
+ list = list.filter(
+ (p) =>
+ p.name.toLowerCase().includes(search.toLowerCase()) ||
+ (p.sku || "").toLowerCase().includes(search.toLowerCase())
+ );
+ }
+
+ // MANUFACTURER FILTER
+ if (manufacturerFilter !== "all") {
+ list = list.filter((p) => p.manufacturer === manufacturerFilter);
+ }
+
+ // CATEGORY FILTER
+ if (categoryFilter !== "all") {
+ list = list.filter((p) => p.category === categoryFilter);
+ }
+
+ // SORTING
+ if (sort === "asc") {
+ list.sort((a, b) => a.name.localeCompare(b.name));
+ } else if (sort === "desc") {
+ list.sort((a, b) => b.name.localeCompare(a.name));
+ }
+
+ return list;
+ }, [parts, search, manufacturerFilter, categoryFilter, sort]);
+
+ // --------------------------------------------------------
+ // 🦴 SKELETON LOADING
+ // --------------------------------------------------------
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+
+ );
}
+ // Collect filter options
+ const manufacturers = Array.from(
+ new Set(parts.map((p) => p.manufacturer).filter(Boolean))
+ );
+
+ const categories = Array.from(
+ new Set(parts.map((p) => p.category).filter(Boolean))
+ );
+
+ // --------------------------------------------------------
+ // 🌟 RENDER UI
+ // --------------------------------------------------------
return (
-
-
-
Parts
-
- + Add Part
-
-
+
+
+ Parts
- {(!parts || parts.length === 0) && (
-
- No parts found yet. Click “Add Part” to create your first
- one.
-
- )}
+
+
- {parts && parts.length > 0 && (
-
-
-
Name
-
Manufacturer
-
SKU
-
Category
-
+
+ {/* SEARCH + FILTERS + SORT BAR */}
+
+ {/* SEARCH */}
+
setSearch(e.target.value)}
+ className="md:w-1/3"
+ />
- {parts.map((p) => (
-
-
{p.name}
-
{p.manufacturer || "-"}
-
{p.sku || "-"}
-
{p.category || "-"}
-
- ))}
+ {/* MANUFACTURER FILTER */}
+
+
+ {/* CATEGORY FILTER */}
+
+
+ {/* SORTING */}
+
- )}
-
+
+ {/* TABLE */}
+
+
+
+ Name
+ Manufacturer
+ Category
+ SKU
+ Actions
+
+
+
+
+ {filteredParts.map((p) => (
+
+ {p.name}
+ {p.manufacturer || "-"}
+ {p.category || "-"}
+ {p.sku || "-"}
+
+
+
+
+
+ ))}
+
+ {filteredParts.length === 0 && (
+
+
+ No parts match your filters.
+
+
+ )}
+
+
+
+
);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index a2dc41e..64b0413 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,26 +1,124 @@
@import "tailwindcss";
-:root {
- --background: #ffffff;
- --foreground: #171717;
-}
+@plugin "tailwindcss-animate";
+@use "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
}
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.129 0.042 264.695);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.129 0.042 264.695);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.129 0.042 264.695);
+ --primary: oklch(0.208 0.042 265.755);
+ --primary-foreground: oklch(0.984 0.003 247.858);
+ --secondary: oklch(0.968 0.007 247.896);
+ --secondary-foreground: oklch(0.208 0.042 265.755);
+ --muted: oklch(0.968 0.007 247.896);
+ --muted-foreground: oklch(0.554 0.046 257.417);
+ --accent: oklch(0.968 0.007 247.896);
+ --accent-foreground: oklch(0.208 0.042 265.755);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.929 0.013 255.508);
+ --input: oklch(0.929 0.013 255.508);
+ --ring: oklch(0.704 0.04 256.788);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.984 0.003 247.858);
+ --sidebar-foreground: oklch(0.129 0.042 264.695);
+ --sidebar-primary: oklch(0.208 0.042 265.755);
+ --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
+ --sidebar-accent: oklch(0.968 0.007 247.896);
+ --sidebar-accent-foreground: oklch(0.208 0.042 265.755);
+ --sidebar-border: oklch(0.929 0.013 255.508);
+ --sidebar-ring: oklch(0.704 0.04 256.788);
+}
+
+.dark {
+ --background: oklch(0.129 0.042 264.695);
+ --foreground: oklch(0.984 0.003 247.858);
+ --card: oklch(0.208 0.042 265.755);
+ --card-foreground: oklch(0.984 0.003 247.858);
+ --popover: oklch(0.208 0.042 265.755);
+ --popover-foreground: oklch(0.984 0.003 247.858);
+ --primary: oklch(0.929 0.013 255.508);
+ --primary-foreground: oklch(0.208 0.042 265.755);
+ --secondary: oklch(0.279 0.041 260.031);
+ --secondary-foreground: oklch(0.984 0.003 247.858);
+ --muted: oklch(0.279 0.041 260.031);
+ --muted-foreground: oklch(0.704 0.04 256.788);
+ --accent: oklch(0.279 0.041 260.031);
+ --accent-foreground: oklch(0.984 0.003 247.858);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.551 0.027 264.364);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.208 0.042 265.755);
+ --sidebar-foreground: oklch(0.984 0.003 247.858);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.984 0.003 247.858);
+ --sidebar-accent: oklch(0.279 0.041 260.031);
+ --sidebar-accent-foreground: oklch(0.984 0.003 247.858);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.551 0.027 264.364);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
}
}
-
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
-}
diff --git a/src/components/ui/badge.jsx b/src/components/ui/badge.jsx
new file mode 100644
index 0000000..a687eba
--- /dev/null
+++ b/src/components/ui/badge.jsx
@@ -0,0 +1,34 @@
+import * as React from "react"
+import { cva } from "class-variance-authority";
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant,
+ ...props
+}) {
+ return (
);
+}
+
+export { Badge, badgeVariants }
diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx
new file mode 100644
index 0000000..ee6f4bd
--- /dev/null
+++ b/src/components/ui/button.jsx
@@ -0,0 +1,48 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva } from "class-variance-authority";
+
+import { cn } from "@/lib/utils"
+
+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",
+ {
+ variants: {
+ variant: {
+ 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",
+ },
+ }
+)
+
+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/card.jsx b/src/components/ui/card.jsx
new file mode 100644
index 0000000..2985cca
--- /dev/null
+++ b/src/components/ui/card.jsx
@@ -0,0 +1,50 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/components/ui/dialog.jsx b/src/components/ui/dialog.jsx
new file mode 100644
index 0000000..0d7981b
--- /dev/null
+++ b/src/components/ui/dialog.jsx
@@ -0,0 +1,96 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogTrigger,
+ DialogClose,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/src/components/ui/input.jsx b/src/components/ui/input.jsx
new file mode 100644
index 0000000..29afa86
--- /dev/null
+++ b/src/components/ui/input.jsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+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/label.jsx b/src/components/ui/label.jsx
new file mode 100644
index 0000000..d3cfbaf
--- /dev/null
+++ b/src/components/ui/label.jsx
@@ -0,0 +1,18 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva } from "class-variance-authority";
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/src/components/ui/select.jsx b/src/components/ui/select.jsx
new file mode 100644
index 0000000..28c363c
--- /dev/null
+++ b/src/components/ui/select.jsx
@@ -0,0 +1,121 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
+
span]:line-clamp-1",
+ className
+ )}
+ {...props}>
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/src/components/ui/separator.jsx b/src/components/ui/separator.jsx
new file mode 100644
index 0000000..9c31480
--- /dev/null
+++ b/src/components/ui/separator.jsx
@@ -0,0 +1,25 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef((
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+) => (
+
+))
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/src/components/ui/skeleton.jsx b/src/components/ui/skeleton.jsx
new file mode 100644
index 0000000..12138e5
--- /dev/null
+++ b/src/components/ui/skeleton.jsx
@@ -0,0 +1,14 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export { Skeleton }
diff --git a/src/components/ui/table.jsx b/src/components/ui/table.jsx
new file mode 100644
index 0000000..07443a1
--- /dev/null
+++ b/src/components/ui/table.jsx
@@ -0,0 +1,86 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Table = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+Table.displayName = "Table"
+
+const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+TableHeader.displayName = "TableHeader"
+
+const TableBody = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+TableBody.displayName = "TableBody"
+
+const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
+
tr]:last:border-b-0", className)}
+ {...props} />
+))
+TableFooter.displayName = "TableFooter"
+
+const TableRow = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+TableRow.displayName = "TableRow"
+
+const TableHead = React.forwardRef(({ className, ...props }, ref) => (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props} />
+))
+TableHead.displayName = "TableHead"
+
+const TableCell = React.forwardRef(({ className, ...props }, ref) => (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props} />
+))
+TableCell.displayName = "TableCell"
+
+const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+TableCaption.displayName = "TableCaption"
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/src/components/ui/textarea.jsx b/src/components/ui/textarea.jsx
new file mode 100644
index 0000000..4d6f7ba
--- /dev/null
+++ b/src/components/ui/textarea.jsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Textarea = React.forwardRef(({ className, ...props }, ref) => {
+ return (
+
+ );
+})
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/src/lib/utils.js b/src/lib/utils.js
new file mode 100644
index 0000000..b20bf01
--- /dev/null
+++ b/src/lib/utils.js
@@ -0,0 +1,6 @@
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs) {
+ return twMerge(clsx(inputs));
+}
|