diff --git a/next.config.ts b/next.config.ts index e9ffa30..fa67b35 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + turbopack: { + root: __dirname, + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 5666e9b..28b1c3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,23 @@ "name": "winofy", "version": "0.1.0", "dependencies": { + "@radix-ui/react-checkbox": "^1.3.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-toast": "^1.2.23", + "@tanstack/react-query": "^5.101.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "jose": "^6.2.9", + "lucide-react": "^1.31.0", "next": "16.3.1", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "tailwind-merge": "^3.6.0", + "zod": "^4.4.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -453,6 +467,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1377,6 +1429,710 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", + "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.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-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.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-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "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.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "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.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "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.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "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.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "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.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.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-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.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-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.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-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.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-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.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-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.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-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "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-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "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-toast": { + "version": "1.2.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.23.tgz", + "integrity": "sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "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.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1676,6 +2432,32 @@ "tailwindcss": "4.3.3" } }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -1722,7 +2504,7 @@ "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1732,7 +2514,7 @@ "version": "19.2.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -2450,6 +3232,18 @@ "dev": true, "license": "Python-2.0" }, + "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==", + "license": "MIT", + "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", @@ -2847,12 +3641,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "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==", + "license": "Apache-2.0", + "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==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2906,7 +3721,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -3041,6 +3856,12 @@ "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==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3955,6 +4776,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4699,6 +5529,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", + "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5155,6 +5994,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.31.0.tgz", + "integrity": "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==", + "license": "ISC", + "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", @@ -5751,6 +6599,75 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6372,6 +7289,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", @@ -6733,6 +7660,49 @@ "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6872,7 +7842,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 41f5269..92d2a3b 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,23 @@ "lint": "eslint" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.3.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-toast": "^1.2.23", + "@tanstack/react-query": "^5.101.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "jose": "^6.2.9", + "lucide-react": "^1.31.0", "next": "16.3.1", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "tailwind-merge": "^3.6.0", + "zod": "^4.4.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..3842cc5 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +type Step = "phone" | "otp"; + +export default function LoginPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const next = searchParams.get("next") ?? "/"; + + const [step, setStep] = useState("phone"); + const [phone, setPhone] = useState(""); + const [otp, setOtp] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleRequestOtp(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + try { + const res = await fetch("/api/auth/request-otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone_number: phone }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.message ?? "خطا در ارسال کد تایید"); + setStep("otp"); + } catch (err) { + setError(err instanceof Error ? err.message : "خطا در ارسال کد تایید"); + } finally { + setLoading(false); + } + } + + async function handleVerifyOtp(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + try { + const res = await fetch("/api/auth/verify-otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone_number: phone, otp }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.message ?? "کد تایید نامعتبر است"); + router.push(next); + router.refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : "کد تایید نامعتبر است"); + } finally { + setLoading(false); + } + } + + return ( +
+
+

ورود به وینوفی

+

+ {step === "phone" + ? "شماره موبایل خود را وارد کنید" + : `کد ارسال شده به ${phone} را وارد کنید`} +

+
+ + {step === "phone" ? ( +
+ setPhone(e.target.value)} + required + className="text-center" + /> + {error &&

{error}

} + +
+ ) : ( +
+ setOtp(e.target.value)} + required + className="text-center tracking-[0.5em]" + /> + {error &&

{error}

} + + +
+ )} +
+ ); +} diff --git a/src/app/(customer)/page.tsx b/src/app/(customer)/page.tsx new file mode 100644 index 0000000..c2acb04 --- /dev/null +++ b/src/app/(customer)/page.tsx @@ -0,0 +1,85 @@ +import Link from "next/link"; +import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy"; +import { resolveMediaUrl } from "@/lib/api/media"; +import type { StoreCategory, StoreListItem } from "@/types/api"; + +async function getStoreCategories() { + const res = await winofyFetch | StoreCategory[]>("/v1/store-categories/", { + auth: false, + }); + return Array.isArray(res) ? res : res.results; +} + +async function getStores() { + const res = await winofyFetch>("/v1/stores/?limit=12", { auth: false }); + return res.results; +} + +export default async function HomePage() { + const [categories, stores] = await Promise.all([getStoreCategories(), getStores()]); + + return ( +
+
+

وینوفی

+ + ورود + +
+ +
+

دسته‌بندی‌ها

+
+ {categories.map((category) => ( +
+ {category.icon && ( + // eslint-disable-next-line @next/next/no-img-element + + )} + {category.name} +
+ ))} + {categories.length === 0 &&

دسته‌بندی‌ای یافت نشد.

} +
+
+ +
+

فروشگاه‌های نزدیک شما

+
+ {stores.map((store) => ( +
+
+ {store.logo ? ( + // eslint-disable-next-line @next/next/no-img-element + {store.name} + ) : ( +
+ )} +
+

{store.name}

+

{store.category.name}

+
+
+
+ ⭐ {Number(store.rating_avg).toFixed(1)} ({store.rating_count}) + {store.is_open ? "باز" : "بسته"} +
+
+ حداقل سفارش: {store.min_order_amount.toLocaleString("fa-IR")} تومان + ارسال: {store.delivery_fee.toLocaleString("fa-IR")} تومان +
+
+ ))} + {stores.length === 0 &&

فروشگاهی یافت نشد.

} +
+
+
+ ); +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..10c5619 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { deleteSession } from "@/lib/auth/session"; + +export async function POST() { + await deleteSession(); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/auth/request-otp/route.ts b/src/app/api/auth/request-otp/route.ts new file mode 100644 index 0000000..001d8e7 --- /dev/null +++ b/src/app/api/auth/request-otp/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { requestOtp, GooyalAuthError } from "@/lib/auth/gooyal"; + +export async function POST(request: Request) { + const body = await request.json().catch(() => null); + const phoneNumber = body?.phone_number; + + if (!phoneNumber || typeof phoneNumber !== "string") { + return NextResponse.json({ message: "شماره موبایل الزامی است." }, { status: 400 }); + } + + try { + const result = await requestOtp(phoneNumber); + return NextResponse.json(result, { status: 201 }); + } catch (err) { + if (err instanceof GooyalAuthError) { + return NextResponse.json({ message: err.message, code: err.code }, { status: err.status }); + } + return NextResponse.json({ message: "سرویس احراز هویت در دسترس نیست." }, { status: 502 }); + } +} diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts new file mode 100644 index 0000000..c396777 --- /dev/null +++ b/src/app/api/auth/verify-otp/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from "next/server"; +import { exchangeOtpForToken, GooyalAuthError } from "@/lib/auth/gooyal"; +import { createSession } from "@/lib/auth/session"; + +export async function POST(request: Request) { + const body = await request.json().catch(() => null); + const phoneNumber = body?.phone_number; + const otp = body?.otp; + + if (!phoneNumber || !otp) { + return NextResponse.json({ message: "شماره موبایل و کد تایید الزامی است." }, { status: 400 }); + } + + try { + const token = await exchangeOtpForToken(phoneNumber, otp); + await createSession({ + accessToken: token.access_token, + refreshToken: token.refresh_token, + tokenType: token.token_type, + expiresAt: Date.now() + token.expires_in * 1000, + }); + return NextResponse.json({ ok: true }); + } catch (err) { + if (err instanceof GooyalAuthError) { + return NextResponse.json({ message: err.message, code: err.code }, { status: err.status }); + } + return NextResponse.json({ message: "سرویس احراز هویت در دسترس نیست." }, { status: 502 }); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..866f6ce 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,52 @@ @import "tailwindcss"; +/* + * Placeholder tokens — TODO replace with real values pulled from the Figma + * design-system page (get_variable_defs) once file access is granted. + */ :root { --background: #ffffff; --foreground: #171717; + --primary: #16a34a; + --primary-foreground: #ffffff; + --secondary: #f1f5f9; + --secondary-foreground: #171717; + --muted: #f1f5f9; + --muted-foreground: #6b7280; + --border: #e5e7eb; + --destructive: #dc2626; + --destructive-foreground: #ffffff; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --font-sans: var(--font-vazirmatn); } @media (prefers-color-scheme: dark) { :root { --background: #0a0a0a; --foreground: #ededed; + --secondary: #1f2937; + --secondary-foreground: #ededed; + --muted: #1f2937; + --muted-foreground: #9ca3af; + --border: #27272a; } } body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans), system-ui, sans-serif; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9852c15..8b9f071 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,29 +1,24 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Vazirmatn } from "next/font/google"; import "./globals.css"; +import { QueryProvider } from "@/lib/query-provider"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], +const vazirmatn = Vazirmatn({ + variable: "--font-vazirmatn", + subsets: ["arabic"], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "وینوفی", + description: "خرید آنلاین از فروشگاه‌های محله شما", }; export default function RootLayout({ children }: LayoutProps<"/">) { return ( - - {children} + + + {children} + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx deleted file mode 100644 index c887311..0000000 --- a/src/app/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the{" "} - - page.tsx - {" "} - file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); -} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..e3bad4f --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,39 @@ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; +import { cn } from "@/lib/utils/cn"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 rounded-xl text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40", + { + variants: { + variant: { + primary: "bg-primary text-primary-foreground hover:bg-primary/90", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + outline: "border border-border bg-transparent hover:bg-muted", + ghost: "bg-transparent hover:bg-muted", + }, + size: { + default: "h-11 px-4", + sm: "h-9 px-3 text-xs", + lg: "h-12 px-6 text-base", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "primary", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +export function Button({ className, variant, size, asChild, ...props }: ButtonProps) { + const Comp = asChild ? Slot : "button"; + return ; +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..a3cd9dc --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { cn } from "@/lib/utils/cn"; + +export const Input = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +Input.displayName = "Input"; diff --git a/src/lib/api/errors.ts b/src/lib/api/errors.ts new file mode 100644 index 0000000..47aede1 --- /dev/null +++ b/src/lib/api/errors.ts @@ -0,0 +1,31 @@ +/** Winofy error envelope, per FRONTEND_GUIDE.md §4.2 */ +export interface WinofyErrorDetails { + message: string | Record; + error?: string; + timestamp?: string; +} + +export class ApiError extends Error { + constructor( + public status: number, + public statusMessage: string, + public details: WinofyErrorDetails | null, + ) { + super(ApiError.extractMessage(details) ?? statusMessage); + } + + private static extractMessage(details: WinofyErrorDetails | null): string | null { + if (!details) return null; + if (typeof details.message === "string") return details.message; + if (details.message && typeof details.message === "object") { + const first = Object.values(details.message)[0]; + return Array.isArray(first) ? first[0] : null; + } + return null; + } + + /** Field-level validation errors, if the envelope's message was keyed by field name. */ + get fieldErrors(): Record | null { + return this.details && typeof this.details.message === "object" ? this.details.message : null; + } +} diff --git a/src/lib/api/media.ts b/src/lib/api/media.ts new file mode 100644 index 0000000..8bcbeff --- /dev/null +++ b/src/lib/api/media.ts @@ -0,0 +1,10 @@ +import "server-only"; + +const API_ORIGIN = new URL(process.env.WINOFY_API_BASE_URL!).origin; + +/** Store/product image fields are `null` or an absolute/relative media URL — see FRONTEND_GUIDE.md §4.5. */ +export function resolveMediaUrl(path: string | null): string | null { + if (!path) return null; + if (path.startsWith("http://") || path.startsWith("https://")) return path; + return API_ORIGIN + (path.startsWith("/") ? path : `/${path}`); +} diff --git a/src/lib/api/winofy.ts b/src/lib/api/winofy.ts new file mode 100644 index 0000000..8e94c70 --- /dev/null +++ b/src/lib/api/winofy.ts @@ -0,0 +1,84 @@ +import "server-only"; +import { getSession, createSession } from "@/lib/auth/session"; +import { refreshAccessToken } from "@/lib/auth/gooyal"; +import { ApiError, type WinofyErrorDetails } from "./errors"; + +const BASE_URL = process.env.WINOFY_API_BASE_URL!; + +interface WinofyFetchOptions extends Omit { + /** Attach the caller's bearer token. Defaults to true — set false for public endpoints. */ + auth?: boolean; + params?: Record; + body?: unknown; +} + +async function getValidAccessToken(): Promise { + const session = await getSession(); + if (!session) return null; + + // 30s skew so we don't hand out a token that expires mid-request. + if (Date.now() < session.expiresAt - 30_000) return session.accessToken; + if (!session.refreshToken) return session.accessToken; + + try { + const refreshed = await refreshAccessToken(session.refreshToken); + await createSession({ + accessToken: refreshed.access_token, + refreshToken: refreshed.refresh_token ?? session.refreshToken, + tokenType: refreshed.token_type, + expiresAt: Date.now() + refreshed.expires_in * 1000, + }); + return refreshed.access_token; + } catch { + return null; + } +} + +export interface PaginatedResponse { + count: number; + next: string | null; + previous: string | null; + results: T[]; +} + +export async function winofyFetch(path: string, options: WinofyFetchOptions = {}): Promise { + const { auth = true, params, headers, body, ...rest } = options; + + const url = new URL(BASE_URL.replace(/\/$/, "") + path); + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) url.searchParams.set(key, String(value)); + } + } + + const finalHeaders = new Headers(headers); + const hasBody = body !== undefined; + if (hasBody && !finalHeaders.has("Content-Type")) { + finalHeaders.set("Content-Type", "application/json"); + } + + if (auth) { + const token = await getValidAccessToken(); + if (token) finalHeaders.set("Authorization", `Bearer ${token}`); + } + + const res = await fetch(url.toString(), { + ...rest, + headers: finalHeaders, + body: hasBody ? JSON.stringify(body) : undefined, + cache: "no-store", + }); + + if (res.status === 204) return undefined as T; + + const data = await res.json().catch(() => null); + + if (!res.ok) { + const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null; + throw new ApiError(res.status, envelope?.status_message ?? res.statusText, envelope?.details ?? null); + } + + return data as T; +} + +export { ApiError }; diff --git a/src/lib/auth/gooyal.ts b/src/lib/auth/gooyal.ts new file mode 100644 index 0000000..f7830bf --- /dev/null +++ b/src/lib/auth/gooyal.ts @@ -0,0 +1,100 @@ +import "server-only"; + +const ACCOUNTS_BASE_URL = process.env.GOOYAL_ACCOUNTS_BASE_URL!; +const CLIENT_ID = process.env.GOOYAL_CLIENT_ID!; +const CLIENT_SECRET = process.env.GOOYAL_CLIENT_SECRET!; +const SCOPE = process.env.GOOYAL_OAUTH_SCOPE!; + +function basicAuthHeader() { + return "Basic " + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64"); +} + +const ERROR_MESSAGES_FA: Record = { + invalid_grant: "کد تایید نامعتبر یا منقضی شده است.", + invalid_client: "خطا در پیکربندی سرویس احراز هویت. لطفاً بعداً تلاش کنید.", + invalid_request: "درخواست نامعتبر است.", + unsupported_grant_type: "خطا در پیکربندی سرویس احراز هویت.", +}; + +export class GooyalAuthError extends Error { + constructor( + public status: number, + public code: string, + description?: string, + ) { + super(ERROR_MESSAGES_FA[code] ?? description ?? code); + } +} + +export interface RequestOtpResult { + phone_number: string; + otp_expire: string; + ttl: number; +} + +export async function requestOtp(phoneNumber: string): Promise { + const res = await fetch(`${ACCOUNTS_BASE_URL}/users/api/request_otp/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone_number: phoneNumber }), + cache: "no-store", + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new GooyalAuthError(res.status, data.error ?? "request_otp_failed", data.error_description ?? data.detail); + } + return data; +} + +export interface GooyalTokenResponse { + access_token: string; + refresh_token?: string; + token_type: string; + expires_in: number; + scope: string; +} + +export async function exchangeOtpForToken(phoneNumber: string, otp: string): Promise { + const body = new URLSearchParams({ + grant_type: "password", + username: phoneNumber, + password: otp, + scope: SCOPE, + auth_fields: "phone_number:otp", + }); + const res = await fetch(`${ACCOUNTS_BASE_URL}/oauth2/token/`, { + method: "POST", + headers: { + Authorization: basicAuthHeader(), + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + cache: "no-store", + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new GooyalAuthError(res.status, data.error ?? "token_exchange_failed", data.error_description); + } + return data; +} + +export async function refreshAccessToken(refreshToken: string): Promise { + const body = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + }); + const res = await fetch(`${ACCOUNTS_BASE_URL}/oauth2/token/`, { + method: "POST", + headers: { + Authorization: basicAuthHeader(), + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + cache: "no-store", + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new GooyalAuthError(res.status, data.error ?? "refresh_failed", data.error_description); + } + return data; +} diff --git a/src/lib/auth/session.ts b/src/lib/auth/session.ts new file mode 100644 index 0000000..8e5d947 --- /dev/null +++ b/src/lib/auth/session.ts @@ -0,0 +1,53 @@ +import "server-only"; +import { EncryptJWT, jwtDecrypt } from "jose"; +import { cookies } from "next/headers"; + +const secret = process.env.SESSION_SECRET; +if (!secret) throw new Error("SESSION_SECRET is not set"); +const encodedKey = new Uint8Array(Buffer.from(secret, "base64")); + +const COOKIE_NAME = "winofy_session"; + +export interface SessionTokens { + accessToken: string; + refreshToken?: string; + tokenType: string; + /** epoch ms */ + expiresAt: number; +} + +export async function createSession(tokens: SessionTokens) { + const jwe = await new EncryptJWT({ ...tokens }) + .setProtectedHeader({ alg: "dir", enc: "A256GCM" }) + .setIssuedAt() + .setExpirationTime("30d") + .encrypt(encodedKey); + + const store = await cookies(); + store.set(COOKIE_NAME, jwe, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 30, + }); +} + +export async function getSession(): Promise { + const store = await cookies(); + const value = store.get(COOKIE_NAME)?.value; + if (!value) return null; + try { + const { payload } = await jwtDecrypt(value, encodedKey); + return payload as unknown as SessionTokens; + } catch { + return null; + } +} + +export async function deleteSession() { + const store = await cookies(); + store.delete(COOKIE_NAME); +} + +export const SESSION_COOKIE_NAME = COOKIE_NAME; diff --git a/src/lib/query-provider.tsx b/src/lib/query-provider.tsx new file mode 100644 index 0000000..f530aef --- /dev/null +++ b/src/lib/query-provider.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useState } from "react"; + +export function QueryProvider({ children }: { children: React.ReactNode }) { + const [client] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + retry: 1, + }, + }, + }), + ); + + return {children}; +} diff --git a/src/lib/utils/cn.ts b/src/lib/utils/cn.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/lib/utils/cn.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..7d9f3c0 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { SESSION_COOKIE_NAME } from "@/lib/auth/session"; + +// Optimistic check only (cookie presence) — real authorization happens per-request +// against the Winofy API, which is the source of truth. See FRONTEND_GUIDE.md §4.3. +const AUTH_REQUIRED_PREFIXES = ["/seller", "/cart", "/checkout", "/orders", "/account", "/notifications"]; + +export default function proxy(request: NextRequest) { + const { pathname } = request.nextUrl; + const hasSession = request.cookies.has(SESSION_COOKIE_NAME); + const needsAuth = AUTH_REQUIRED_PREFIXES.some((prefix) => pathname.startsWith(prefix)); + + if (needsAuth && !hasSession) { + const loginUrl = new URL("/login", request.url); + loginUrl.searchParams.set("next", pathname); + return NextResponse.redirect(loginUrl); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/((?!api|_next/static|_next/image|.*\\.(?:png|jpg|jpeg|svg|ico)$).*)"], +}; diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000..7cebc1d --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,181 @@ +/** Shared API types mirrored from FRONTEND_GUIDE.md. Money fields are integer Toman. */ + +export interface City { + uuid: string; + name: string; +} + +export interface Neighborhood { + uuid: string; + name: string; + city: string; +} + +export interface StoreCategory { + uuid: string; + name: string; + icon: string | null; + order: number; +} + +export interface StoreListItem { + uuid: string; + name: string; + category: StoreCategory; + logo: string | null; + cover_image: string | null; + /** DRF serializes DecimalField as a string, e.g. "3.76" — confirmed against staging. */ + rating_avg: string; + rating_count: number; + min_order_amount: number; + delivery_fee: number; + free_delivery_threshold: number | null; + is_open: boolean; +} + + +export interface WorkingHours { + weekday: 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Saturday .. 6 = Friday + opens_at: string; + closes_at: string; + is_closed: boolean; +} + +export interface StoreDetail extends StoreListItem { + description: string; + address: string; + phone_number: string; + working_hours: WorkingHours[]; + accepts_wallet: boolean; + accepts_online: boolean; + accepts_cash_on_delivery: boolean; +} + +export type UnitType = "gram" | "ml" | "piece"; + +export interface ProductCategory { + uuid: string; + name: string; + parent: string | null; +} + +export interface ProductListItem { + uuid: string; + name: string; + image: string | null; + price: number; + unit_type: UnitType; + unit_value: number; + is_active: boolean; + is_out_of_stock: boolean; +} + +export interface ProductDetail extends ProductListItem { + description: string; + category: ProductCategory; + store: StoreListItem; +} + +export type AddressLabel = "home" | "work" | "other"; + +export interface Address { + uuid: string; + label: AddressLabel; + title: string; + full_address: string; + plaque: string; + floor: string; + unit: string; + recipient_name: string; + recipient_phone: string; + city: City; + neighborhood: Neighborhood | null; + latitude: number; + longitude: number; + is_default: boolean; + created_at: string; +} + +export interface CartItem { + uuid: string; + product: ProductListItem; + quantity: number; + line_total: number; +} + +export interface CartGroup { + store: StoreListItem; + items: CartItem[]; + items_subtotal: number; + delivery_fee: number; + total: number; + meets_minimum_order: boolean; +} + +export interface Cart { + groups: CartGroup[]; + grand_total: number; +} + +export type DeliveryType = "express" | "scheduled"; +export type PaymentMethod = "wallet" | "online" | "cash_on_delivery"; +export type PaymentStatus = "pending" | "paid" | "failed"; +export type OrderStatus = + | "placed" + | "preparing" + | "ready_to_ship" + | "handed_to_courier" + | "delivered" + | "cancelled"; + +export interface OrderItem { + uuid: string; + product: string; + product_name_snapshot: string; + unit_price_snapshot: number; + quantity: number; + line_total: number; +} + +export interface OrderStatusLog { + from_status: string; + to_status: string; + note: string; + created_at: string; +} + +export interface Order { + uuid: string; + store: StoreListItem; + status: OrderStatus; + items: OrderItem[]; + items_subtotal: number; + delivery_fee: number; + commission_amount: number; + seller_payout_amount: number; + total_amount: number; + cancel_reason: string; + courier_name: string; + courier_phone: string; + placed_at: string; + delivered_at: string | null; + status_logs: OrderStatusLog[]; + is_cancellable: boolean; +} + +export interface OrderGroup { + uuid: string; + recipient_name: string; + recipient_phone: string; + full_address: string; + delivery_type: DeliveryType; + scheduled_at: string | null; + payment_method: PaymentMethod; + payment_status: PaymentStatus; + notes: string; + total_amount: number; + orders: Order[]; + created_at: string; + /** `feature/payment` only. */ + payment?: { method: PaymentMethod; status: string; payment_url?: string }; +}