diff --git a/.commandcode/taste/taste/taste.md b/.commandcode/taste/taste/taste.md index 8e22427..e157291 100644 --- a/.commandcode/taste/taste/taste.md +++ b/.commandcode/taste/taste/taste.md @@ -30,6 +30,7 @@ - Prefers detailed technical documentation of system architecture, data models, flows, and integrations in markdown format. Confidence: 0.8 - Wants edge cases explicitly enumerated when documenting or analyzing existing code/systems. Confidence: 0.8 - Values end-to-end analysis of features (architecture, data model, flows, integrations, and edge cases) when asked to explain how something works. Confidence: 0.7 +- When creating a clone/mirror of an app on a different platform (e.g., a web version of a React Native app), expects EXACT replication — functionality and looks must match the reference app with zero changes ("not even a slight change is acceptable"). The tech stack is inherited from the existing clone, not the original. Confidence: 0.9 - Prefers unifying parallel data structures into a single instance rather than maintaining separate ones (e.g., one cart for flash and regular items instead of separate flash/regular carts). Confidence: 0.9 - Prefers using sentinel values in existing fields (e.g., slotId = 0) to distinguish special-case items rather than creating separate fields or structures. Confidence: 0.9 - Prefers handling special-case logic locally in the relevant component/file rather than globally or via parallel flows. Confidence: 0.85 diff --git a/apps/admin-web/.cta.json b/apps/admin-web/.cta.json new file mode 100644 index 0000000..ccd23b8 --- /dev/null +++ b/apps/admin-web/.cta.json @@ -0,0 +1,19 @@ +{ + "projectName": "web-ui", + "mode": "file-router", + "typescript": true, + "tailwind": true, + "packageManager": "bun", + "git": true, + "install": true, + "intent": true, + "addOnOptions": {}, + "includeExamples": false, + "envVarValues": {}, + "routerOnly": false, + "version": 1, + "framework": "react", + "chosenAddOns": [ + "nitro" + ] +} \ No newline at end of file diff --git a/apps/admin-web/README.md b/apps/admin-web/README.md new file mode 100644 index 0000000..fd4381f --- /dev/null +++ b/apps/admin-web/README.md @@ -0,0 +1,15 @@ +# Freshyo Admin Web + +Web clone of `apps/admin-ui` — same screens, same functionality, same looks — +built on the exact stack of `apps/web-ui` (TanStack Start + Vite + Tailwind v4 + +tRPC + TanStack Router). + +```bash +npm install +npm run dev # http://localhost:4175 +npm run build # production build +npm run typecheck +``` + +Backend: same as admin-ui (`https://devapi.freshyo.in`); override with +`VITE_API_URL` env var. diff --git a/apps/admin-web/change-log.txt b/apps/admin-web/change-log.txt new file mode 100644 index 0000000..ce93400 --- /dev/null +++ b/apps/admin-web/change-log.txt @@ -0,0 +1,9 @@ + +[2026-09-06 00:10:00] admin-web integration fixes batch: (1) dashboard.notifications.tsx SearchableSelect(number[] mismatch) -> MultiSelect (matches original multi BottomDropdown); (2) TagMenu.tsx router.history.push string nav -> typed navigate({to search}); (3) dashboard.orders.$id.tsx user tap /dashboard/users -> /dashboard/users/$id params nav (route exists); (4) dashboard.users.$id.tsx order nav as-any -> typed params nav; (5) remove temp as-any on now-registered navigate literals (dashboard.index, orders hub, slots, banners, stores, prices, products.edit/groupings casts if tsc-clean); (6) regen routeTree.gen.ts via build. + +[2026-09-06 00:20:00] admin-web README: replaced TanStack boilerplate with Freshyo Admin Web run/build notes (npm install/dev/build, port 4175, VITE_API_URL override). +[2026-09-06 00:20:00] COMPLETED apps/admin-web: exact web clone of apps/admin-ui on apps/web-ui stack (TanStack Start + Vite + Tailwind v4 + tRPC + TanStack Router, port 4175, worker admin-web). 38 routes + 21 components + infra, tsc clean, vite build clean, SSR smoke 200s. +Routes: index gate, login, dashboard shell (sidebar/header/titles/refresh) + hub, complaints, coupons(+new,+$id/edit), customize-app(+popular,+ordering), banners(+new,+$id/edit), orders(+list,+sequence,+$id), prices, product-groupings(+new,+$id/edit), product-tags(+new,+edit?tagId,+order), products(+new,+edit?id,+$id), rebalance, notifications, slots(+new?baseslot,+$id/edit,+detail?slotId), stores(+new,+edit?id), users(+$id), vendor-snippets. +New local primitives: MultiSelect, SearchableSelect, DateTimeInput, DateInput (mirror RN BottomDropdown-multi/search + DateTimePickerMod/DatePicker APIs). +Verification: tsc 0 errors; 54/54 file pairs tRPC-procedure parity (2 apparent gaps proven dead code in original: updateAddressCoords declaration, refund commented block); build ok; dev SSR / /login /dashboard /dashboard/coupons 200, /dashboard/orders/sequence 307 redirect, 0 error refs. +Known platform deltas (no web equivalent): Alert.alert->window.alert, destructive confirms->window.confirm, pull-to-refresh->header refresh button, infinite scroll->Load-more buttons, expo-image-picker->file inputs, native date/time pickers->date+time inputs, haptics/blur/share-target->dropped or navigator.share w/ clipboard fallback, push-notification token acquisition dropped (send flow intact). diff --git a/apps/admin-web/package-lock.json b/apps/admin-web/package-lock.json new file mode 100644 index 0000000..e9b792d --- /dev/null +++ b/apps/admin-web/package-lock.json @@ -0,0 +1,6596 @@ +{ + "name": "admin-web", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "admin-web", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-devtools": "^0.10.2", + "@tanstack/react-query": "^5.100.0", + "@tanstack/react-query-devtools": "^5.100.0", + "@tanstack/react-router": "^1.167.0", + "@tanstack/react-router-devtools": "^1.166.0", + "@tanstack/react-router-ssr-query": "^1.166.0", + "@tanstack/react-start": "^1.167.0", + "@tanstack/router-plugin": "^1.167.0", + "@trpc/client": "^11.17.0", + "@trpc/react-query": "^11.17.0", + "@trpc/server": "^11.17.0", + "axios": "^1.16.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "dayjs": "^1.11.20", + "formik": "^2.4.9", + "fuse.js": "^7.3.0", + "jwt-decode": "^4.0.0", + "lucide-react": "^0.400.0", + "nitro": "npm:nitro-nightly@latest", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sonner": "^1.7.4", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.1.18", + "yup": "^1.7.1", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.36.3", + "@tanstack/devtools-vite": "^0.6.0", + "@types/node": "^22.10.2", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "^5.8.3", + "vite": "^8.0.0", + "vitest": "^4.1.5", + "wrangler": "^4.90.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vite-plugin": { + "version": "1.36.3", + "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.36.3.tgz", + "integrity": "sha512-n3SZhEZQxk4B2xHaCwgXfc7oLkx/4leTxL254P09DK2Qoj1VVOqVELo62CsUYq5dZS+okMdeWqNa13xEOKMs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudflare/unenv-preset": "2.16.1", + "miniflare": "4.20260507.1", + "unenv": "2.0.0-rc.24", + "wrangler": "4.90.0", + "ws": "8.18.0" + }, + "peerDependencies": { + "vite": "^6.1.0 || ^7.0.0 || ^8.0.0", + "wrangler": "^4.90.0" + } + }, + "node_modules/@cloudflare/vite-plugin/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260507.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260507.1.tgz", + "integrity": "sha512-S85aMwcaPJUjKWDiG6iMMnioKWtPLACa6m0j/EhHR1GYfVpnxb974cBc6d25L+sf7jHWHJI2u5hGp0UTJ7MtXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260507.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260507.1.tgz", + "integrity": "sha512-GMEBu8Zp9Q97HLnf7bWJN4KjWpN5MxpeqdvHjBGWNl8UYprJI0k+Jkp89+Wh5S8vIon+HoVbDfOzPa7VwgL6Eg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260507.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260507.1.tgz", + "integrity": "sha512-QlrKEBdgA3uVc0Ok0Q3+0/CW0CTjgj5ySir1i1YY5FXVv0X6GpwtnB5umjunjF2MFprss+L+iFGZzxcSvMC1nA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260507.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260507.1.tgz", + "integrity": "sha512-eGbbupEtK2nh9V9Dhcx3vv3GTKeXqSVNgAEYVCCN0NGS9tl9HbMoHRX/4JL181FKXROMigWBCQVL//qPhsAzBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260507.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260507.1.tgz", + "integrity": "sha512-dmClJ/E0BAcuDetQIZFqbeAXejWrG5pysGRMQ6T83Y0IW/7IAamY2zFEkAJ10I5xwZsdHuYsZtzlOxpEXpJs7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oozcitak/dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", + "license": "MIT", + "dependencies": { + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", + "license": "MIT", + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.129.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", + "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", + "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", + "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", + "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", + "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", + "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", + "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", + "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.40.tgz", + "integrity": "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@solid-primitives/event-listener": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.4.5.tgz", + "integrity": "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA==", + "license": "MIT", + "dependencies": { + "@solid-primitives/utils": "^6.4.0" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/keyboard": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@solid-primitives/keyboard/-/keyboard-1.3.5.tgz", + "integrity": "sha512-sav+l+PL+74z3yaftVs7qd8c2SXkqzuxPOVibUe5wYMt+U5Hxp3V3XCPgBPN2I6cANjvoFtz0NiU8uHVLdi9FQ==", + "license": "MIT", + "dependencies": { + "@solid-primitives/event-listener": "^2.4.5", + "@solid-primitives/rootless": "^1.5.3", + "@solid-primitives/utils": "^6.4.0" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/resize-observer": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@solid-primitives/resize-observer/-/resize-observer-2.1.5.tgz", + "integrity": "sha512-AiyTknKcNBaKHbcSMuxtSNM8FjIuiSuFyFghdD0TcCMU9hKi9EmsC5pjfjDwxE+5EueB1a+T/34PLRI5vbBbKw==", + "license": "MIT", + "dependencies": { + "@solid-primitives/event-listener": "^2.4.5", + "@solid-primitives/rootless": "^1.5.3", + "@solid-primitives/static-store": "^0.1.3", + "@solid-primitives/utils": "^6.4.0" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/rootless": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@solid-primitives/rootless/-/rootless-1.5.3.tgz", + "integrity": "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA==", + "license": "MIT", + "dependencies": { + "@solid-primitives/utils": "^6.4.0" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/static-store": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.1.3.tgz", + "integrity": "sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ==", + "license": "MIT", + "dependencies": { + "@solid-primitives/utils": "^6.4.0" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/utils": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.4.0.tgz", + "integrity": "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A==", + "license": "MIT", + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", + "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", + "devOptional": true, + "license": "CC0-1.0" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/devtools": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/devtools/-/devtools-0.11.2.tgz", + "integrity": "sha512-K8+tsBx+ptTLqqd4dOF10B6laj1g+XYImqYZL9n0jBINGaT+sOf17PKV9pbBt8kdbZeIGsHaJ5OZWCyZoHqN4A==", + "license": "MIT", + "dependencies": { + "@solid-primitives/event-listener": "^2.4.3", + "@solid-primitives/keyboard": "^1.3.3", + "@solid-primitives/resize-observer": "^2.1.3", + "@tanstack/devtools-client": "0.0.6", + "@tanstack/devtools-event-bus": "0.4.1", + "@tanstack/devtools-ui": "0.5.1", + "clsx": "^2.1.1", + "goober": "^2.1.16", + "solid-js": "^1.9.9" + }, + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "solid-js": ">=1.9.7" + } + }, + "node_modules/@tanstack/devtools-client": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-client/-/devtools-client-0.0.6.tgz", + "integrity": "sha512-f85ZJXJnDIFOoykG/BFIixuAevJovCvJF391LPs6YjBAPhGYC50NWlx1y4iF/UmK5/cCMx+/JqI5SBOz7FanQQ==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-event-client": "^0.4.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/devtools-event-bus": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-bus/-/devtools-event-bus-0.4.1.tgz", + "integrity": "sha512-cNnJ89Q021Zf883rlbBTfsaxTfi2r73/qejGtyTa7ksErF3hyDyAq1aTbo5crK9dAL7zSHh9viKY1BtMls1QOA==", + "license": "MIT", + "dependencies": { + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/devtools-event-client": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.3.tgz", + "integrity": "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==", + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/devtools-ui": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-ui/-/devtools-ui-0.5.1.tgz", + "integrity": "sha512-T9JjAdqMSnxsVO6AQykD5vhxPF4iFLKtbYxee/bU3OLlk446F5C1220GdCmhDSz7y4lx+m8AvIS0bq6zzvdDUA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "dayjs": "^1.11.19", + "goober": "^2.1.16", + "solid-js": "^1.9.9" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "solid-js": ">=1.9.7" + } + }, + "node_modules/@tanstack/devtools-vite": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-vite/-/devtools-vite-0.6.0.tgz", + "integrity": "sha512-h0r0ct7zlrgjkhmn4QW6wRjgUXd4JMs+r7gtx+BXo9f5H9Y+jtUdtvC0rnZcPto6gw/9yMUq7yOmMK5qDWRExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/generator": "^7.28.3", + "@babel/parser": "^7.28.4", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@tanstack/devtools-client": "0.0.6", + "@tanstack/devtools-event-bus": "0.4.1", + "chalk": "^5.6.2", + "launch-editor": "^2.11.1", + "picomatch": "^4.0.3" + }, + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@tanstack/history": { + "version": "1.161.6", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.161.6.tgz", + "integrity": "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz", + "integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.9.tgz", + "integrity": "sha512-gqiptrTIhbK2PuCaPRHmWXfJG1NGYVFpAr0HqogEqiSBNB5xDz6fmesQt7w4WgMOqOQPnPHJ3ZDMuhDaXvNO8g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-devtools": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.10.2.tgz", + "integrity": "sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools": "0.11.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "@types/react-dom": ">=16.8", + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz", + "integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.100.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.9.tgz", + "integrity": "sha512-mM3slaVGXJmz+pOLgXdANj75ikgQCyudyl3kmFvm6brI1JyVeY/+IeD17uDHIvZrD8hfoO2sdZ54RFsHdYAuhA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.100.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.100.9", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.169.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.169.2.tgz", + "integrity": "sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "@tanstack/react-store": "^0.9.3", + "@tanstack/router-core": "1.169.2", + "isbot": "^5.1.22" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-router-devtools": { + "version": "1.166.13", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.166.13.tgz", + "integrity": "sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA==", + "license": "MIT", + "dependencies": { + "@tanstack/router-devtools-core": "1.167.3" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.168.15", + "@tanstack/router-core": "^1.168.11", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/router-core": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-router-ssr-query": { + "version": "1.166.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-ssr-query/-/react-router-ssr-query-1.166.12.tgz", + "integrity": "sha512-yDUIoEh+PimAcWmk/2BE0EkI8TwLVeToNzoIuwahmTtBUR+ptZPWbtiPjudO8JZ0BhT3odHtuOn1eBOK0/4NAQ==", + "license": "MIT", + "dependencies": { + "@tanstack/router-ssr-query-core": "1.168.0" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5.90.0", + "@tanstack/react-query": ">=5.90.0", + "@tanstack/react-router": ">=1.127.0", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-start": { + "version": "1.167.65", + "resolved": "https://registry.npmjs.org/@tanstack/react-start/-/react-start-1.167.65.tgz", + "integrity": "sha512-vCGga3RECeR4VpSVuXIU/+zxak5f2qdpUXdZ2yrgcwwKoYPtatdJm6zjS0Py7UOecRqLqMtSeuOjowBJ1higWQ==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router": "1.169.2", + "@tanstack/react-start-client": "1.166.48", + "@tanstack/react-start-rsc": "0.0.44", + "@tanstack/react-start-server": "1.166.52", + "@tanstack/router-utils": "1.161.8", + "@tanstack/start-client-core": "1.168.2", + "@tanstack/start-plugin-core": "1.169.20", + "@tanstack/start-server-core": "1.167.30", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rsbuild/core": "^2.0.0", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0", + "vite": ">=7.0.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "@vitejs/plugin-rsc": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-start-client": { + "version": "1.166.48", + "resolved": "https://registry.npmjs.org/@tanstack/react-start-client/-/react-start-client-1.166.48.tgz", + "integrity": "sha512-6fqwCwe6v+Nvtdf6vg6gxs/0gCXyZEHF18EslNeG/kca2wnXYFuXRhqGJjJaEgMk3WF4IE9mUgFuBSAOY3P7nQ==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router": "1.169.2", + "@tanstack/router-core": "1.169.2", + "@tanstack/start-client-core": "1.168.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-start-rsc": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/@tanstack/react-start-rsc/-/react-start-rsc-0.0.44.tgz", + "integrity": "sha512-5iYUWSBjTwJbV8bTLJHZ5dHm8c/79J6spxPlKsjt9/R0mQaQQjLVNMpv5CrOZ2vPTaZx1ALoGdSWP4WdPcuKRA==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router": "1.169.2", + "@tanstack/react-start-server": "1.166.52", + "@tanstack/router-core": "1.169.2", + "@tanstack/router-utils": "1.161.8", + "@tanstack/start-client-core": "1.168.2", + "@tanstack/start-fn-stubs": "1.161.6", + "@tanstack/start-plugin-core": "1.169.20", + "@tanstack/start-server-core": "1.167.30", + "@tanstack/start-storage-context": "1.166.35", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rspack/core": ">=2.0.0-0", + "@vitejs/plugin-rsc": ">=0.5.20", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0", + "react-server-dom-rspack": ">=0.0.2" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "@vitejs/plugin-rsc": { + "optional": true + }, + "react-server-dom-rspack": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-start-server": { + "version": "1.166.52", + "resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.166.52.tgz", + "integrity": "sha512-46Gx+byIndYywUtyna5h3qatHipJkPFqo/miexfuYPgeVAI6ypQzsw7wxF194H6VAP43m2q+fdLPBXStufoOGw==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "@tanstack/react-router": "1.169.2", + "@tanstack/router-core": "1.169.2", + "@tanstack/start-client-core": "1.168.2", + "@tanstack/start-server-core": "1.167.30" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", + "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.9.3", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.169.2", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.169.2.tgz", + "integrity": "sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "cookie-es": "^3.0.0", + "seroval": "^1.5.4", + "seroval-plugins": "^1.5.4" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-devtools-core": { + "version": "1.167.3", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.167.3.tgz", + "integrity": "sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/router-core": "^1.168.11", + "csstype": "^3.0.10" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-generator": { + "version": "1.166.42", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.166.42.tgz", + "integrity": "sha512-2qBWC0t78r6b3vI+AbnvCZcFAvbYBDlLuWZrTjQbcjUmwG3qyeQp983tJyDuj9wb5//adG1tgAGXZkJ3aDwdBg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.169.2", + "@tanstack/router-utils": "1.161.8", + "@tanstack/virtual-file-routes": "1.161.7", + "jiti": "^2.7.0", + "magic-string": "^0.30.21", + "prettier": "^3.5.0", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-plugin": { + "version": "1.167.35", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.167.35.tgz", + "integrity": "sha512-UAScU5VAzLYVY4FML/Cbc5S5TucT4I8Ata05yozGOe4ZfepTKRffA5xWLtD2N+ov5svdv0KTX/kqlZnYPe28mA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.169.2", + "@tanstack/router-generator": "1.166.42", + "@tanstack/router-utils": "1.161.8", + "@tanstack/virtual-file-routes": "1.161.7", + "chokidar": "^3.6.0", + "unplugin": "^3.0.0", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rsbuild/core": ">=1.0.2 || ^2.0.0", + "@tanstack/react-router": "^1.169.2", + "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", + "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", + "webpack": ">=5.92.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "vite": { + "optional": true + }, + "vite-plugin-solid": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-ssr-query-core": { + "version": "1.168.0", + "resolved": "https://registry.npmjs.org/@tanstack/router-ssr-query-core/-/router-ssr-query-core-1.168.0.tgz", + "integrity": "sha512-5yBUAF1d9z2kOFKoz1spvpvkMSTmRnRXEwi+bGKfrXYmt7CfHu3Pk8KUFMln67uQoKQ9VTkcd5tLkjJVrZ2/AQ==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5.90.0", + "@tanstack/router-core": ">=1.127.0" + } + }, + "node_modules/@tanstack/router-utils": { + "version": "1.161.8", + "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.161.8.tgz", + "integrity": "sha512-xyiLWEKjfBAVhauDSSjXxyf7s8elU6SM+V050sbkofvGmIIvkwPFtDsX7Gvwh14kBd6iCwAT+RiPvXTxAptY0Q==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "ansis": "^4.1.0", + "babel-dead-code-elimination": "^1.0.12", + "diff": "^8.0.2", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.15" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/start-client-core": { + "version": "1.168.2", + "resolved": "https://registry.npmjs.org/@tanstack/start-client-core/-/start-client-core-1.168.2.tgz", + "integrity": "sha512-/bckv9k/yxY4VmSY2V2MeX7NBsS5uqGvdSPs5WIvW3Uv35DXPrdiumKXTNJeZRNRMtxrM+YfxQPjXLx3C7ykvg==", + "license": "MIT", + "dependencies": { + "@tanstack/router-core": "1.169.2", + "@tanstack/start-fn-stubs": "1.161.6", + "@tanstack/start-storage-context": "1.166.35", + "seroval": "^1.5.4" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/start-fn-stubs": { + "version": "1.161.6", + "resolved": "https://registry.npmjs.org/@tanstack/start-fn-stubs/-/start-fn-stubs-1.161.6.tgz", + "integrity": "sha512-Y6QSlGiLga8cHfvxGGaonXIlt2bIUTVdH6AMjmpMp7+ANNCp+N96GQbjjhLye3JkaxDfP68x5iZA8NK4imgRig==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/start-plugin-core": { + "version": "1.169.20", + "resolved": "https://registry.npmjs.org/@tanstack/start-plugin-core/-/start-plugin-core-1.169.20.tgz", + "integrity": "sha512-MLSH5P3auFpnol1lMGQhUrpJH7+P5knzBXMnJjXG+nVOvmcYbY0JA+nQMl81kKiqfkEceAiaEdKhl8Zc5Ldolw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.27.1", + "@babel/core": "^7.28.5", + "@babel/types": "^7.28.5", + "@rolldown/pluginutils": "1.0.0-beta.40", + "@tanstack/router-core": "1.169.2", + "@tanstack/router-generator": "1.166.42", + "@tanstack/router-plugin": "1.167.35", + "@tanstack/router-utils": "1.161.8", + "@tanstack/start-client-core": "1.168.2", + "@tanstack/start-server-core": "1.167.30", + "cheerio": "^1.0.0", + "exsolve": "^1.0.7", + "lightningcss": "^1.32.0", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "seroval": "^1.5.4", + "source-map": "^0.7.6", + "srvx": "^0.11.9", + "tinyglobby": "^0.2.15", + "ufo": "^1.5.4", + "vitefu": "^1.1.1", + "xmlbuilder2": "^4.0.3", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rsbuild/core": "^2.0.0", + "vite": ">=7.0.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@tanstack/start-plugin-core/node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@tanstack/start-server-core": { + "version": "1.167.30", + "resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.167.30.tgz", + "integrity": "sha512-GC0PXzYYSEwfAOC2NxGXFUyYvfbSjVoqnIrzJsyInKd8xQxGEQaVdrebbyx9TV5cj7A5e7EJcWAsf3G3wRDQBw==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.161.6", + "@tanstack/router-core": "1.169.2", + "@tanstack/start-client-core": "1.168.2", + "@tanstack/start-storage-context": "1.166.35", + "fetchdts": "^0.1.6", + "h3-v2": "npm:h3@2.0.1-rc.20", + "seroval": "^1.5.4" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/start-storage-context": { + "version": "1.166.35", + "resolved": "https://registry.npmjs.org/@tanstack/start-storage-context/-/start-storage-context-1.166.35.tgz", + "integrity": "sha512-ZKDkKiorJrKwfEHjatEwRHG7EP3raJPhh6CSl4CFmHW0naIvwaW5gQcxcT8IlHtoGDLYDAjBEcSr3MZyXgqmOA==", + "license": "MIT", + "dependencies": { + "@tanstack/router-core": "1.169.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", + "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-file-routes": { + "version": "1.161.7", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.161.7.tgz", + "integrity": "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==", + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@trpc/client": { + "version": "11.17.0", + "resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.17.0.tgz", + "integrity": "sha512-KpJBFrbKTDeVCFv/3ckL1XBBH5Yssn8hethI/rUy7GIpTj+VzjtPjykDqJpzobuVOz+d26cXCSu1t4I6MYI5Zg==", + "funding": [ + "https://trpc.io/sponsor" + ], + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "peerDependencies": { + "@trpc/server": "11.17.0", + "typescript": ">=5.7.2" + } + }, + "node_modules/@trpc/react-query": { + "version": "11.17.0", + "resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-11.17.0.tgz", + "integrity": "sha512-AGcl5YAF8NnhBmyJ6PqJqKb1M5VTGSoNRNqJ3orct4o4epdcg0GWhW+qT9q6gPzs/2ImIwYCdfFpgNGdZ9yLHA==", + "funding": [ + "https://trpc.io/sponsor" + ], + "license": "MIT", + "peerDependencies": { + "@tanstack/react-query": "^5.80.3", + "@trpc/client": "11.17.0", + "@trpc/server": "11.17.0", + "react": ">=18.2.0", + "typescript": ">=5.7.2" + } + }, + "node_modules/@trpc/server": { + "version": "11.17.0", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.17.0.tgz", + "integrity": "sha512-jbAOUe0PpUTCYqziyu+8vYXZdDXPudZgnEhWCQ2NjKnVEjfE93RqHTt1oycZJv/HNf51YlRXfEEwSIAbb161rw==", + "funding": [ + "https://trpc.io/sponsor" + ], + "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, + "peerDependencies": { + "typescript": ">=5.7.2" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", + "integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", + "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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/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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.4.5.tgz", + "integrity": "sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA==", + "license": "MIT", + "peerDependencies": { + "srvx": ">=0.11.5" + }, + "peerDependenciesMeta": { + "srvx": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/db0": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/db0/-/db0-0.3.4.tgz", + "integrity": "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==", + "license": "MIT", + "peerDependencies": { + "@electric-sql/pglite": "*", + "@libsql/client": "*", + "better-sqlite3": "*", + "drizzle-orm": "*", + "mysql2": "*", + "sqlite3": "*" + }, + "peerDependenciesMeta": { + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "license": "ISC" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", + "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-runner": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/env-runner/-/env-runner-0.1.7.tgz", + "integrity": "sha512-i7h96jxETJYhXy5grgHNJ9xNzCzWIn9Ck/VkkYgOlE4gOqknsLX3CmlVb5LmwNex8sOoLFVZLz+TIw/+b5rktA==", + "license": "MIT", + "dependencies": { + "crossws": "^0.4.4", + "exsolve": "^1.0.8", + "httpxy": "^0.5.0", + "srvx": "^0.11.13" + }, + "bin": { + "env-runner": "dist/cli.mjs" + }, + "peerDependencies": { + "@netlify/runtime": "^4", + "miniflare": "^4.20260317.3" + }, + "peerDependenciesMeta": { + "@netlify/runtime": { + "optional": true + }, + "miniflare": { + "optional": true + } + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetchdts": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/fetchdts/-/fetchdts-0.1.7.tgz", + "integrity": "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formik": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.9.tgz", + "integrity": "sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz", + "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/krisk" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "2.0.1-rc.22", + "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.22.tgz", + "integrity": "sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==", + "license": "MIT", + "dependencies": { + "rou3": "^0.8.1", + "srvx": "^0.11.15" + }, + "bin": { + "h3": "bin/h3.mjs" + }, + "engines": { + "node": ">=20.11.1" + }, + "peerDependencies": { + "crossws": "^0.4.1" + }, + "peerDependenciesMeta": { + "crossws": { + "optional": true + } + } + }, + "node_modules/h3-v2": { + "name": "h3", + "version": "2.0.1-rc.20", + "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.20.tgz", + "integrity": "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==", + "license": "MIT", + "dependencies": { + "rou3": "^0.8.1", + "srvx": "^0.11.13" + }, + "bin": { + "h3": "bin/h3.mjs" + }, + "engines": { + "node": ">=20.11.1" + }, + "peerDependencies": { + "crossws": "^0.4.1" + }, + "peerDependenciesMeta": { + "crossws": { + "optional": true + } + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/httpxy": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/httpxy/-/httpxy-0.5.1.tgz", + "integrity": "sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isbot": { + "version": "5.1.40", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.40.tgz", + "integrity": "sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/launch-editor": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", + "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.400.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.400.0.tgz", + "integrity": "sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ==", + "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", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/miniflare": { + "version": "4.20260507.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260507.1.tgz", + "integrity": "sha512-PSXBiLExTdZ4UGO/raKCHQauUpYL7F880ZRB7j0+78Rv8h7TsdN2E/iEDK9sK2Y+SPQ5wJSeAa+rDeVKoZZoEw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.24.8", + "workerd": "1.20260507.1", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/miniflare/node_modules/undici": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/miniflare/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nf3": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/nf3/-/nf3-0.3.17.tgz", + "integrity": "sha512-N9zEWySuJFw+gR0lhS5863YsvNeudOdqRyFvNb+jMXbeTJOdrjDqkCpDginIZfUm0LzT1t1nCRiDeqQm/8kirQ==", + "license": "MIT" + }, + "node_modules/nitro": { + "name": "nitro-nightly", + "version": "3.0.1-20260508-123613-d2e64906", + "resolved": "https://registry.npmjs.org/nitro-nightly/-/nitro-nightly-3.0.1-20260508-123613-d2e64906.tgz", + "integrity": "sha512-vqVPALHX0Couvdvdm9QZs0dHcbLqiZxlNqq0iQcQzzToEzmORCPYx26Fj4iaOSpvXMaf0uc0i6iMsDvFrlYGnQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.4.2", + "crossws": "^0.4.5", + "db0": "^0.3.4", + "env-runner": "^0.1.7", + "h3": "^2.0.1-rc.22", + "hookable": "^6.1.1", + "nf3": "^0.3.16", + "ocache": "^0.1.4", + "ofetch": "^2.0.0-alpha.3", + "ohash": "^2.0.11", + "rolldown": "^1.0.0", + "srvx": "^0.11.15", + "unenv": "^2.0.0-rc.24", + "unstorage": "^2.0.0-alpha.7" + }, + "bin": { + "nitro": "dist/cli/index.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@vercel/queue": "^0.1.6", + "dotenv": "*", + "giget": "*", + "jiti": "^2.6.1", + "rollup": "^4.60.3", + "vite": "^7 || ^8", + "xml2js": "^0.6.2", + "zephyr-agent": "^0.2.0" + }, + "peerDependenciesMeta": { + "@vercel/queue": { + "optional": true + }, + "dotenv": { + "optional": true + }, + "giget": { + "optional": true + }, + "jiti": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "xml2js": { + "optional": true + }, + "zephyr-agent": { + "optional": true + } + } + }, + "node_modules/nitro/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nitro/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "license": "BlueOak-1.0.0", + "optional": true, + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/nitro/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nitro/node_modules/unstorage": { + "version": "2.0.0-alpha.7", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-2.0.0-alpha.7.tgz", + "integrity": "sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog==", + "license": "MIT", + "peerDependencies": { + "@azure/app-configuration": "^1.11.0", + "@azure/cosmos": "^4.9.1", + "@azure/data-tables": "^13.3.2", + "@azure/identity": "^4.13.0", + "@azure/keyvault-secrets": "^4.10.0", + "@azure/storage-blob": "^12.31.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.13.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.36.2", + "@vercel/blob": ">=0.27.3", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1.0.1", + "aws4fetch": "^1.0.20", + "chokidar": "^4 || ^5", + "db0": ">=0.3.4", + "idb-keyval": "^6.2.2", + "ioredis": "^5.9.3", + "lru-cache": "^11.2.6", + "mongodb": "^6 || ^7", + "ofetch": "*", + "uploadthing": "^7.7.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "chokidar": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "lru-cache": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "ofetch": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/ocache": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ocache/-/ocache-0.1.4.tgz", + "integrity": "sha512-e7geNdWjxSnvsSgvLuPvgKgu7ubM10ZmTPOgpr7mz2BXYtvjMKTiLhjFi/gWU8chkuP6hNkZBsa9LzOusyaqkQ==", + "license": "MIT", + "dependencies": { + "ohash": "^2.0.11" + } + }, + "node_modules/ofetch": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-2.0.0-alpha.3.tgz", + "integrity": "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==", + "license": "MIT" + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", + "license": "MIT" + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rolldown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", + "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.129.0", + "@rolldown/pluginutils": "1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0", + "@rolldown/binding-darwin-arm64": "1.0.0", + "@rolldown/binding-darwin-x64": "1.0.0", + "@rolldown/binding-freebsd-x64": "1.0.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", + "@rolldown/binding-linux-arm64-gnu": "1.0.0", + "@rolldown/binding-linux-arm64-musl": "1.0.0", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0", + "@rolldown/binding-linux-s390x-gnu": "1.0.0", + "@rolldown/binding-linux-x64-gnu": "1.0.0", + "@rolldown/binding-linux-x64-musl": "1.0.0", + "@rolldown/binding-openharmony-arm64": "1.0.0", + "@rolldown/binding-wasm32-wasi": "1.0.0", + "@rolldown/binding-win32-arm64-msvc": "1.0.0", + "@rolldown/binding-win32-x64-msvc": "1.0.0" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", + "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", + "license": "MIT" + }, + "node_modules/rou3": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.8.1.tgz", + "integrity": "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", + "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", + "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/solid-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", + "integrity": "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.5.0", + "seroval-plugins": "~1.5.0" + } + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/srvx": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.15.tgz", + "integrity": "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==", + "license": "MIT", + "bin": { + "srvx": "bin/srvx.mjs" + }, + "engines": { + "node": ">=20.16.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", + "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0-rc.18", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@oxc-project/types": { + "version": "0.128.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", + "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", + "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", + "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", + "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", + "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", + "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", + "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", + "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", + "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "license": "MIT" + }, + "node_modules/vite/node_modules/rolldown": { + "version": "1.0.0-rc.18", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", + "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.128.0", + "@rolldown/pluginutils": "1.0.0-rc.18" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", + "@rolldown/binding-darwin-x64": "1.0.0-rc.18", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20260507.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260507.1.tgz", + "integrity": "sha512-z7JhsFSe6+X1b5fUHaVpo15VM1IRMJiLofEkq8iKdCo+Veqc+FUg5lIsuz8NwePxuSKrXtO4ZQpGkQLbPVXFhg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260507.1", + "@cloudflare/workerd-darwin-arm64": "1.20260507.1", + "@cloudflare/workerd-linux-64": "1.20260507.1", + "@cloudflare/workerd-linux-arm64": "1.20260507.1", + "@cloudflare/workerd-windows-64": "1.20260507.1" + } + }, + "node_modules/wrangler": { + "version": "4.90.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.90.0.tgz", + "integrity": "sha512-bmNIykl59TfCUn5xQgU7IWylSsPx3LQaPLMSAq2VQHt89CBrcj9qXQ0eYfjBCWA5XTBVgten391evt7xxtXwcA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260507.1", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260507.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260507.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlbuilder2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", + "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/apps/admin-web/package.json b/apps/admin-web/package.json new file mode 100644 index 0000000..13ba458 --- /dev/null +++ b/apps/admin-web/package.json @@ -0,0 +1,60 @@ +{ + "name": "admin-web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "npm run build && wrangler dev", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "deploy": "npm run build && wrangler deploy", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-devtools": "^0.10.2", + "@tanstack/react-query": "^5.100.0", + "@tanstack/react-query-devtools": "^5.100.0", + "@tanstack/react-router": "^1.167.0", + "@tanstack/react-router-devtools": "^1.166.0", + "@tanstack/react-router-ssr-query": "^1.166.0", + "@tanstack/react-start": "^1.167.0", + "@tanstack/router-plugin": "^1.167.0", + "@trpc/client": "^11.17.0", + "@trpc/react-query": "^11.17.0", + "@trpc/server": "^11.17.0", + "axios": "^1.16.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.20", + "date-fns": "^4.1.0", + "formik": "^2.4.9", + "fuse.js": "^7.3.0", + "jwt-decode": "^4.0.0", + "lucide-react": "^0.400.0", + "nitro": "npm:nitro-nightly@latest", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sonner": "^1.7.4", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.1.18", + "yup": "^1.7.1", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.36.3", + "@tanstack/devtools-vite": "^0.6.0", + "@types/node": "^22.10.2", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "^5.8.3", + "vite": "^8.0.0", + "vitest": "^4.1.5", + "wrangler": "^4.90.0" + } +} diff --git a/apps/admin-web/public/favicon.png b/apps/admin-web/public/favicon.png new file mode 100644 index 0000000..83f0a8c Binary files /dev/null and b/apps/admin-web/public/favicon.png differ diff --git a/apps/admin-web/public/freshyo-logo.png b/apps/admin-web/public/freshyo-logo.png new file mode 100644 index 0000000..83f0a8c Binary files /dev/null and b/apps/admin-web/public/freshyo-logo.png differ diff --git a/apps/admin-web/public/logo192.png b/apps/admin-web/public/logo192.png new file mode 100644 index 0000000..c1c782e Binary files /dev/null and b/apps/admin-web/public/logo192.png differ diff --git a/apps/admin-web/public/logo512.png b/apps/admin-web/public/logo512.png new file mode 100644 index 0000000..2053b8b Binary files /dev/null and b/apps/admin-web/public/logo512.png differ diff --git a/apps/admin-web/public/manifest.json b/apps/admin-web/public/manifest.json new file mode 100644 index 0000000..a3831d6 --- /dev/null +++ b/apps/admin-web/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Freshyo", + "name": "Freshyo", + "icons": [ + { + "src": "favicon.png", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/png" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#0033CC", + "background_color": "#ffffff" +} diff --git a/apps/admin-web/public/robots.txt b/apps/admin-web/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/apps/admin-web/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/admin-web/src/client.tsx b/apps/admin-web/src/client.tsx new file mode 100644 index 0000000..e5ac4b1 --- /dev/null +++ b/apps/admin-web/src/client.tsx @@ -0,0 +1,4 @@ +import { hydrateRoot } from 'react-dom/client' +import { StartClient } from '@tanstack/react-start/client' + +hydrateRoot(document, ) diff --git a/apps/admin-web/src/components/BannerForm.tsx b/apps/admin-web/src/components/BannerForm.tsx new file mode 100644 index 0000000..b0755a7 --- /dev/null +++ b/apps/admin-web/src/components/BannerForm.tsx @@ -0,0 +1,225 @@ +import { useRef, useState, type ChangeEvent } from 'react' +import { Formik } from 'formik' +import * as Yup from 'yup' +import { p as P, pInput as PInput, ImageUploader } from 'web-components' +import ProductsSelector from './ProductsSelector' +import { trpc } from '@/lib/trpc-client' +import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStorage' + +export interface BannerFormData { + name: string + imageUrl: string + description: string + skuIds: number[] + redirectUrl: string + // serialNum removed - will be assigned automatically by backend +} + +interface BannerFormProps { + initialValues: BannerFormData + onSubmit: (values: BannerFormData, imageUrl?: string) => Promise | void + onCancel: () => void + submitButtonText?: string + isSubmitting?: boolean + existingImageUrl?: string + mode?: 'create' | 'edit' +} + +const validationSchema = Yup.object().shape({ + name: Yup.string().trim().required('Banner name is required').max(255), + description: Yup.string().max(500), + skuIds: Yup.array() + .of(Yup.number()) + .optional(), + redirectUrl: Yup.string() + .url('Please enter a valid URL') + .optional(), + // serialNum validation removed - assigned automatically by backend +}) + +export default function BannerForm({ + initialValues, + onSubmit, + onCancel, + submitButtonText = 'Create Banner', + isSubmitting = false, + existingImageUrl, + mode = 'create', +}: BannerFormProps) { + const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]) + const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]) + const fileInputRef = useRef(null) + + const { uploadSingle } = useUploadToObjectStorage() + + // Fetch products for dropdown + const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery() + const products = productsData?.products || [] + + const handleImagePick = () => { + fileInputRef.current?.click() + } + + const handleFileChange = (e: ChangeEvent) => { + const files = Array.from(e.target.files || []) + if (files.length === 0) { + setSelectedImages([]) + setDisplayImages([]) + return + } + const file = files[0] + setSelectedImages([{ blob: file, mimeType: file.type || 'image/jpeg' }]) + setDisplayImages([{ uri: URL.createObjectURL(file) }]) + e.target.value = '' + } + + const handleRemoveImage = (uri: string) => { + const index = displayImages.findIndex(img => img.uri === uri) + if (index !== -1) { + const newDisplay = displayImages.filter((_, i) => i !== index) + const newFiles = selectedImages.filter((_, i) => i !== index) + setDisplayImages(newDisplay) + setSelectedImages(newFiles) + } + } + + const handleFormikSubmit = async (values: BannerFormData) => { + try { + let imageUrl: string | undefined + + if (selectedImages.length > 0) { + const { blob, mimeType } = selectedImages[0] + const { presignedUrl } = await uploadSingle(blob, mimeType, 'store') + imageUrl = presignedUrl + } + + await onSubmit(values, imageUrl) + } catch (error) { + console.error('Upload error:', error) + window.alert('Error: Failed to upload image') + } + } + + return ( + + {({ + handleChange, + handleBlur, + handleSubmit, + values, + errors, + touched, + isValid, + dirty, + setFieldValue, + }) => ( +
+ +
+ + {errors.name && touched.name && ( +

{errors.name}

+ )} + +
+

Banner Image

+ { + // Handle removing existing image in edit mode + }} + allowMultiple={false} + /> +
+ + + {errors.description && touched.description && ( +

{errors.description}

+ )} + +
+ { + const selectedValues = Array.isArray(value) ? value : [value] + setFieldValue('skuIds', selectedValues.map(v => Number(v))) + }} + multiple={true} + label="Select Products" + placeholder="Select products for banner (optional)" + labelFormat={(product) => `${product.productName} (₹${product.price})`} + /> +
+ + + {errors.redirectUrl && touched.redirectUrl && ( +

{errors.redirectUrl}

+ )} + + {/* Action Buttons */} +
+ + + +
+
+
+ )} +
+ ) +} diff --git a/apps/admin-web/src/components/CancelOrderDialog.tsx b/apps/admin-web/src/components/CancelOrderDialog.tsx new file mode 100644 index 0000000..e6e4b89 --- /dev/null +++ b/apps/admin-web/src/components/CancelOrderDialog.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react' +import type { OrderDialogBaseProps } from '@packages/shared' +import { p as P, BottomDialog, pInput as PInput } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import { XCircle } from 'lucide-react' + +interface CancelOrderDialogProps extends OrderDialogBaseProps { + onSuccess?: () => void +} + +export default function CancelOrderDialog({ orderId, open, onClose, onSuccess }: CancelOrderDialogProps) { + const [cancelReason, setCancelReason] = useState('') + const cancelOrderMutation = trpc.admin.order.cancelOrder.useMutation() + + const handleCancel = () => { + if (!cancelReason.trim()) { + window.alert('Error: Please enter a cancellation reason') + return + } + + const confirmed = window.confirm( + 'Cancel Order? Are you sure you want to cancel this order? This action cannot be undone.' + ) + if (!confirmed) return + + cancelOrderMutation.mutate( + { orderId, reason: cancelReason }, + { + onSuccess: () => { + onClose() + setCancelReason('') + onSuccess?.() + }, + onError: (error: any) => { + window.alert(`Error: ${error.message || 'Failed to cancel order'}`) + }, + } + ) + } + + return ( + +
+
+
+ +
+

+ Cancel Order +

+

+ This will cancel the order and mark it as cancelled by admin. A refund record will be created. +

+
+ + setCancelReason(e.target.value)} + placeholder="Enter reason for cancellation..." + multiline + className="h-24" + /> + +
+ + +
+
+
+ ) +} diff --git a/apps/admin-web/src/components/CouponForm.tsx b/apps/admin-web/src/components/CouponForm.tsx new file mode 100644 index 0000000..1dfda47 --- /dev/null +++ b/apps/admin-web/src/components/CouponForm.tsx @@ -0,0 +1,558 @@ +import { useState, useEffect, type ChangeEvent } from 'react' +import { Formik } from 'formik' +import * as Yup from 'yup' +import { pInput as PInput, MyButton, AppContainer, Checkbox, BottomDialog, p as P, MyFlatList } from 'web-components' +import DateTimeInput from './DateTimeInput' +import { trpc } from '@/lib/trpc-client' + +// Local copy of common-ui/shared-types CreateCouponPayload (web cannot import +// the RN common-ui package; @packages/shared does not export this type). +export interface CreateCouponPayload { + couponCodes: string[] + discountPercent?: number + flatDiscount?: number + minOrder?: number + maxValue?: number + validTill?: string + maxLimitForUser?: number + isApplyForAll: boolean + isUserBased: boolean + targetUsers?: number[] + productIds?: number[] + skuIds?: number[] + applicableUsers?: number[] + applicableProducts?: number[] + exclusiveApply?: boolean +} + +const USERS_PAGE_SIZE = 10 + +interface CouponFormValues extends CreateCouponPayload { + isReservedCoupon?: boolean + couponCodes: string[] + skuIds?: number[] +} + +interface CouponFormProps { + onSubmit: (values: CouponFormValues) => void + isLoading: boolean + initialValues?: Partial +} + +const couponValidationSchema = Yup.object().shape({ + isReservedCoupon: Yup.boolean().optional(), + couponCodes: Yup.array().of( + Yup.string() + .required('Code is required') + .min(3, 'Code must be at least 3 characters') + .max(50, 'Code cannot exceed 50 characters') + .matches(/^[A-Z0-9_-]+$/, 'Code can only contain uppercase letters, numbers, underscores, and hyphens') + ), + discountPercent: Yup.number() + .min(0, 'Must be positive') + .max(100, 'Cannot exceed 100%') + .optional(), + flatDiscount: Yup.number() + .min(0, 'Must be positive') + .optional(), + minOrder: Yup.number().min(0, 'Must be positive').optional(), + maxValue: Yup.number().min(0, 'Must be positive').optional(), + validTill: Yup.date().optional(), + maxLimitForUser: Yup.number().min(1, 'Must be at least 1').optional(), + exclusiveApply: Yup.boolean().optional(), + isUserBased: Yup.boolean(), + isApplyForAll: Yup.boolean(), + applicableUsers: Yup.array().of(Yup.number()).optional(), +}).test('discount-validation', 'Must provide exactly one discount type with valid value', function(value) { + const { discountPercent, flatDiscount } = value + const hasPercent = discountPercent !== undefined && discountPercent > 0 + const hasFlat = flatDiscount !== undefined && flatDiscount > 0 + + if (hasPercent && hasFlat) { + return this.createError({ message: 'Cannot have both percentage and flat discount' }) + } + if (!hasPercent && !hasFlat) { + return this.createError({ message: 'Must provide either percentage or flat discount' }) + } + return true +}).test('codes-required', 'At least one coupon code is required', function(value: any) { + const codes = (value.couponCodes || []).filter((c: string) => c && c.trim()) + if (codes.length === 0) { + return this.createError({ path: 'couponCodes', message: 'Add at least one coupon code' }) + } + return true +}).test('codes-unique', 'Coupon codes must be unique', function(value: any) { + const codes = (value.couponCodes || []).map((c: string) => c.trim()).filter(Boolean) + if (new Set(codes).size !== codes.length) { + return this.createError({ path: 'couponCodes', message: 'Coupon codes must be unique' }) + } + return true +}) + +export default function CouponForm({ onSubmit, isLoading, initialValues }: CouponFormProps) { + + // User dropdown states + const [userSearchQuery, setUserSearchQuery] = useState('') + const [userOffset, setUserOffset] = useState(0) + const [allUsers, setAllUsers] = useState<{ id: number; name: string; mobile: string | null }[]>([]) + const [hasMoreUsers, setHasMoreUsers] = useState(true) + const [usersDropdownOpen, setUsersDropdownOpen] = useState(false) + + // Add-multiple dialog states (for pattern codes like ABC12) + const [multiDialogOpen, setMultiDialogOpen] = useState(false) + const [multiCount, setMultiCount] = useState('') + + const { data: usersData, isFetching: isFetchingUsers } = trpc.admin.coupon.getUsersMiniInfo.useQuery( + { search: userSearchQuery, limit: USERS_PAGE_SIZE, offset: userOffset }, + { enabled: usersDropdownOpen } + ) + + useEffect(() => { + if (usersData?.users) { + if (userOffset === 0) { + setAllUsers(usersData.users) + } else { + setAllUsers(prev => [...prev, ...usersData.users]) + } + setHasMoreUsers(usersData.users.length === USERS_PAGE_SIZE) + } + }, [usersData, userOffset]) + + useEffect(() => { + setUserOffset(0) + setHasMoreUsers(true) + }, [userSearchQuery]) + + useEffect(() => { + if (usersDropdownOpen) { + setUserOffset(0) + setAllUsers([]) + setHasMoreUsers(true) + setUserSearchQuery('') + } + }, [usersDropdownOpen]) + + // User search functionality will be inside Formik + + const defaultValues: CouponFormValues = { + couponCodes: [''], + isUserBased: false, + isApplyForAll: false, + targetUsers: [], + discountPercent: undefined, + flatDiscount: undefined, + minOrder: undefined, + maxValue: undefined, + validTill: undefined, + maxLimitForUser: undefined, + skuIds: undefined, + applicableUsers: [], + applicableProducts: [], + exclusiveApply: false, + isReservedCoupon: false, + } + + return ( + + initialValues={(initialValues || defaultValues) as CouponFormValues} + validationSchema={couponValidationSchema} + onSubmit={onSubmit} + > + {({ values, errors, touched, setFieldValue, handleSubmit }) => { + + const toggleUserSelection = (userId: number) => { + const current = values.applicableUsers || [] + const newSelection = current.includes(userId) + ? current.filter(id => id !== userId) + : [...current, userId] + setFieldValue('applicableUsers', newSelection) + } + + const isReserved = (values as any).isReservedCoupon + + // Detect pattern codes like ABC12 (ends in digits) for bulk generation. + // Continue numbering from the highest code with the same prefix. + const patternCode = (values.couponCodes || []).find( + (c: string) => /^[A-Z0-9_-]*\d+$/.test(c.trim()) + ) + const patternPrefix = patternCode ? patternCode.replace(/\d+$/, '') : '' + const patternStart = (values.couponCodes || []).reduce((max, c) => { + const trimmed = c.trim() + if (!patternPrefix || !trimmed.startsWith(patternPrefix)) return max + const m = trimmed.match(/\d+$/) + if (!m) return max + return Math.max(max, parseInt(m[0], 10)) + }, patternCode ? parseInt((patternCode.match(/\d+$/) || ['0'])[0], 10) : 0) + + const handleAddMultiple = () => { + const count = parseInt(multiCount, 10) + if (!count || count < 1 || count > 100) return + // Start from the next number after the highest existing code + const newCodes = Array.from( + { length: count }, + (_, i) => `${patternPrefix}${patternStart + 1 + i}` + ) + const existing = (values.couponCodes || []).filter((c: string) => c && c.trim()) + setFieldValue('couponCodes', [...existing, ...newCodes]) + setMultiDialogOpen(false) + setMultiCount('') + } + + return ( + + {/* Is Reserved Coupon Checkbox */} +
+ setFieldValue('isReservedCoupon', !(values as any).isReservedCoupon)} + /> +

Is Reserved Coupon

+
+ + {/* Coupon Codes */} +
+

Coupon Codes

+

+ Each code creates a separate coupon with the same settings. +

+ {(values.couponCodes || []).map((code: string, index: number) => { + const codeError = Array.isArray(errors.couponCodes) + ? (errors.couponCodes as any)[index] + : errors.couponCodes + return ( +
+
+ ) => { + const next = [...(values.couponCodes || [])] + next[index] = e.target.value.toUpperCase() + setFieldValue('couponCodes', next) + }} + error={!!codeError} + /> + {codeError &&

{codeError}

} +
+ +
+ ) + })} + {(errors.couponCodes as any)?.message && ( +

{(errors.couponCodes as any).message}

+ )} +
+ + + {patternCode && ( + + )} +
+
+ + {/* Discount Type Selection */} +

+ Discount Type * +

+ +
+ + + +
+ + {/* Discount Value */} + {values.discountPercent !== undefined && ( +
+ setFieldValue('discountPercent', parseFloat(e.target.value) || 0)} + type="number" + inputMode="numeric" + error={!!(touched.discountPercent && errors.discountPercent)} + /> +
+ )} + + {values.flatDiscount !== undefined && ( +
+ setFieldValue('flatDiscount', parseFloat(e.target.value) || 0)} + type="number" + inputMode="numeric" + error={!!(touched.flatDiscount && errors.flatDiscount)} + /> +
+ )} + + {/* Minimum Order */} +
+ setFieldValue('minOrder', parseFloat(e.target.value) || undefined)} + type="number" + inputMode="numeric" + error={!!(touched.minOrder && errors.minOrder)} + /> +
+ + {/* Maximum Discount Value - only for percentage discounts */} + {values.flatDiscount === undefined && ( +
+ setFieldValue('maxValue', parseFloat(e.target.value) || undefined)} + type="number" + inputMode="numeric" + error={!!(touched.maxValue && errors.maxValue)} + /> +
+ )} + + {/* Validity Period */} +
+

Valid Till

+ { + + setFieldValue('validTill', date?.toISOString()) + }} + /> +
+ + {/* Usage Limit */} +
+ setFieldValue('maxLimitForUser', parseInt(e.target.value) || undefined)} + type="number" + inputMode="numeric" + error={!!(touched.maxLimitForUser && errors.maxLimitForUser)} + /> +
+ + {/* Exclusive Apply */} +
+

Exclusive Apply

+ +
+ + {/* Target Audience */} +

+ Target Audience {isReserved ? '(Disabled for Reserved Coupons)' : ''} +

+ +
+ + + +
+ + {/* Applicable User Selection */} +
+

Applicable Users (Optional)

+ +
+ + setUsersDropdownOpen(false)}> +
+

Select Applicable Users

+ setUserSearchQuery(e.target.value)} + /> +
+ item.id.toString()} + renderItem={(item) => ( + + )} + ListFooterComponent={ + <> + {isFetchingUsers ?

Loading...

: null} + {hasMoreUsers && !isFetchingUsers ? ( + + ) : null} + + } + /> +
+ +
+
+ + {/* Add Multiple Coupons Dialog */} + setMultiDialogOpen(false)}> +
+

Add Multiple Coupons

+

+ Generate {patternCode ? `${patternPrefix}${patternStart + 1}, ${patternPrefix}${patternStart + 2}, ...` : 'sequential'} codes from your pattern +

+ setMultiCount(e.target.value)} + type="number" + inputMode="numeric" + /> +
+ + +
+
+
+ + {/* Submit Button */} + handleSubmit()} + disabled={isLoading} + textContent={isLoading ? 'Creating...' : 'Create Coupon'} + /> +
+ ) + }} + + ) +} diff --git a/apps/admin-web/src/components/DateInput.tsx b/apps/admin-web/src/components/DateInput.tsx new file mode 100644 index 0000000..a7ef9dd --- /dev/null +++ b/apps/admin-web/src/components/DateInput.tsx @@ -0,0 +1,31 @@ +import { p as P } from 'web-components' + +interface DateInputProps { + value: Date | null + setValue: (d: Date | null) => void + showLabel?: boolean + placeholder?: string +} + +function toDateStr(d: Date | null): string { + if (!d) return '' + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` +} + +export default function DateInput({ value, setValue, showLabel = true, placeholder = 'Select Date' }: DateInputProps) { + return ( +
+ {showLabel ?

{placeholder}

: null} + setValue(e.target.value ? new Date(`${e.target.value}T00:00:00`) : null)} + placeholder={placeholder} + className="w-full rounded border border-gray-300 px-2 py-2 text-sm font-medium text-gray-800" + /> +
+ ) +} diff --git a/apps/admin-web/src/components/DateTimeInput.tsx b/apps/admin-web/src/components/DateTimeInput.tsx new file mode 100644 index 0000000..2260dac --- /dev/null +++ b/apps/admin-web/src/components/DateTimeInput.tsx @@ -0,0 +1,80 @@ +import { p as P } from 'web-components' + +interface DateTimeInputProps { + value: Date | null + setValue: (d: Date | null) => void + showLabels?: boolean + timeOnly?: boolean +} + +function toDateStr(d: Date | null): string { + if (!d) return '' + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` +} + +function toTimeStr(d: Date | null): string { + if (!d) return '' + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}` +} + +export default function DateTimeInput({ value, setValue, showLabels = true, timeOnly = false }: DateTimeInputProps) { + const handleDateChange = (dateStr: string) => { + if (!dateStr) { + setValue(null) + return + } + const base = value ? new Date(value) : new Date() + const [y, m, day] = dateStr.split('-').map((n) => parseInt(n, 10)) + base.setFullYear(y, m - 1, day) + setValue(base) + } + + const handleTimeChange = (timeStr: string) => { + if (!timeStr) return + const base = value ? new Date(value) : new Date() + const [h, min] = timeStr.split(':').map((n) => parseInt(n, 10)) + base.setHours(h, min, 0, 0) + setValue(base) + } + + if (timeOnly) { + return ( +
+ {showLabels ?

Select Time

: null} + handleTimeChange(e.target.value)} + className="w-full rounded border border-gray-300 px-2 py-1 text-sm font-medium text-gray-800" + /> +
+ ) + } + + return ( +
+
+ {showLabels ?

Select Date

: null} + handleDateChange(e.target.value)} + className="w-full rounded border border-gray-300 px-2 py-1 text-sm font-medium text-gray-800" + /> +
+
+
+ {showLabels ?

Select Time

: null} + handleTimeChange(e.target.value)} + className="w-full rounded border border-gray-300 px-2 py-1 text-sm font-medium text-gray-800" + /> +
+
+ ) +} diff --git a/apps/admin-web/src/components/MultiSelect.tsx b/apps/admin-web/src/components/MultiSelect.tsx new file mode 100644 index 0000000..2c430a4 --- /dev/null +++ b/apps/admin-web/src/components/MultiSelect.tsx @@ -0,0 +1,151 @@ +import { useMemo, useState } from 'react' +import { BottomDialog, Checkbox, SearchBar, p as P } from 'web-components' +import { Check, ChevronDown } from 'lucide-react' + +export interface MultiSelectOption { + label: string + value: string | number + disabled?: boolean +} + +interface MultiSelectProps { + label?: string + topLabel?: string + options: MultiSelectOption[] + value: (string | number)[] + onValueChange: (v: (string | number)[]) => void + placeholder?: string + disabled?: boolean + onSearch?: (q: string) => void + error?: boolean + className?: string + testID?: string + // Tolerated for screen compatibility (RN BottomDropdown leftovers) — ignored on web + multiple?: boolean + triggerComponent?: any +} + +export function MultiSelect({ + label, + topLabel, + options, + value, + onValueChange, + placeholder, + disabled = false, + onSearch, + error = false, + className, + testID, +}: MultiSelectProps) { + const [open, setOpen] = useState(false) + const [query, setQuery] = useState('') + + const selectedSet = useMemo(() => new Set(value.map((v) => String(v))), [value]) + + const displayText = useMemo(() => { + if (value.length === 0) return placeholder ?? label ?? 'Select...' + if (value.length === 1) { + const found = options.find((o) => String(o.value) === String(value[0])) + return found ? found.label : (placeholder ?? label ?? 'Select...') + } + return `${value.length} selected` + }, [value, options, placeholder, label]) + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase() + if (!q) return options + return options.filter((o) => o.label.toLowerCase().includes(q)) + }, [options, query]) + + const toggle = (optionValue: string | number) => { + const key = String(optionValue) + if (selectedSet.has(key)) { + onValueChange(value.filter((v) => String(v) !== key)) + } else { + onValueChange([...value, optionValue]) + } + } + + const heading = topLabel ?? label + + return ( +
+ {heading ? ( +

{heading}

+ ) : null} + + setOpen(false)}> +
+

{heading ?? placeholder ?? 'Select'}

+ { + setQuery(q) + onSearch?.(q) + }} + onSearch={onSearch} + className="mb-3" + /> +
+ {filtered.map((option) => { + const checked = selectedSet.has(String(option.value)) + return ( +
{ + if (!option.disabled) toggle(option.value) + }} + className={`flex flex-row items-center p-3 border-b border-gray-100 ${option.disabled ? 'opacity-50' : 'cursor-pointer hover:bg-gray-50'}`} + > + e.stopPropagation()}> + { + if (!option.disabled) toggle(option.value) + }} + /> + + {option.label} + {checked ? : null} +
+ ) + })} + {filtered.length === 0 ? ( +

No options found

+ ) : null} +
+
+ + +
+
+
+
+ ) +} + +export default MultiSelect diff --git a/apps/admin-web/src/components/OrderNotesForm.tsx b/apps/admin-web/src/components/OrderNotesForm.tsx new file mode 100644 index 0000000..4c736f4 --- /dev/null +++ b/apps/admin-web/src/components/OrderNotesForm.tsx @@ -0,0 +1,70 @@ +import { useState, type FC } from 'react' +import { p as P, MyButton, pInput as PInput } from 'web-components' +import { trpc } from '@/lib/trpc-client' + +interface OrderNotesFormProps { + orderId: number + initialNotes?: string + onSuccess?: () => void + onCancel?: () => void +} + +export const OrderNotesForm: FC = ({ + orderId, + initialNotes = '', + onSuccess, + onCancel, +}) => { + const [notes, setNotes] = useState(initialNotes) + const updateNotesMutation = trpc.admin.order.updateNotes.useMutation() + + const handleSubmit = async () => { + if (!notes.trim()) { + window.alert('Error: Please enter some notes') + return + } + + try { + await updateNotesMutation.mutateAsync({ + orderId, + adminNotes: notes.trim(), + }) + window.alert('Success: Notes updated successfully') + onSuccess?.() + } catch (error) { + window.alert('Error: Failed to update notes') + } + } + + return ( +
+

+ Add Order Notes +

+ + setNotes(e.target.value)} + placeholder="Enter notes for this order..." + multiline + numberOfLines={4} + className="min-h-24" + /> + +
+ + +
+
+ ) +} diff --git a/apps/admin-web/src/components/OrderOptionsMenu.tsx b/apps/admin-web/src/components/OrderOptionsMenu.tsx new file mode 100644 index 0000000..87c1d0c --- /dev/null +++ b/apps/admin-web/src/components/OrderOptionsMenu.tsx @@ -0,0 +1,327 @@ +import React from 'react' +import { p as P, BottomDialog, Checkbox } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import { ChevronRight, Eye, Pencil, XCircle, MapPin, Map, MessageCircle, Phone } from 'lucide-react' + +interface OrderOptionsMenuProps { + open: boolean + onClose: () => void + order: { + id: number + readableId: number + isPackaged: boolean + isDelivered: boolean + isFlashDelivery?: boolean + address: string + addressId: number + adminNotes?: string | null + userNotes?: string | null + latitude?: number | null + longitude?: number | null + status?: string + } + onViewDetails: () => void + onTogglePackaged: () => void + onToggleDelivered: () => void + onOpenAdminNotes: () => void + onCancelOrder: () => void + onAttachLocation: () => void + onWhatsApp: () => void + onDial: () => void +} + +function getCurrentPosition(): Promise { + return new Promise((resolve, reject) => { + if (!navigator.geolocation) { + reject(new Error('Geolocation is not supported by this browser')) + return + } + navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: true }) + }) +} + +export function OrderOptionsMenu({ + open, + onClose, + order, + onViewDetails, + onTogglePackaged, + onToggleDelivered, + onOpenAdminNotes, + onCancelOrder, + onAttachLocation, + onWhatsApp, + onDial, +}: OrderOptionsMenuProps) { + const updateAddressCoordsMutation = trpc.admin.order.updateAddressCoords.useMutation() + + const handleAttachLocation = async () => { + try { + const location = await getCurrentPosition() + const { latitude, longitude } = location.coords + await updateAddressCoordsMutation.mutateAsync({ + addressId: order.addressId, + latitude, + longitude, + }) + window.alert('Success: Location attached to address successfully.') + onAttachLocation() + } catch (error: any) { + if (error?.code === 1) { + window.alert('Permission Denied: Location permission is required to attach coordinates.') + return + } + window.alert('Error: Failed to attach location. Please try again.') + } + } + + const extractPhone = (address: string) => { + const phoneMatch = address.match(/Phone: (\d+)/) + return phoneMatch ? phoneMatch[1] : null + } + + const handleWhatsApp = () => { + const phone = extractPhone(order.address) + if (phone) { + window.open(`https://wa.me/91${phone}`, '_blank') + } else { + window.alert('No phone number found') + } + } + + const handleDial = () => { + const phone = extractPhone(order.address) + if (phone) { + window.open(`tel:${phone}`, '_blank') + } else { + window.alert('No phone number found') + } + } + + const handleOpenInMaps = () => { + if (order.latitude && order.longitude) { + const url = `https://www.google.com/maps/search/?api=1&query=${order.latitude},${order.longitude}` + window.open(url, '_blank') + } else { + window.alert('No location coordinates available') + } + } + + const hasCoordinates = order.latitude !== null && order.latitude !== undefined && + order.longitude !== null && order.longitude !== undefined + + return ( + +
+
+
+
+
+

+ Order #{order.readableId} +

+

+ Select an action to perform +

+
+ + + +
+ + +
+ +
+ + +
+ + + + {order.status !== 'cancelled' && ( + + )} + + + + {hasCoordinates && ( + + )} + + + + +
+
+
+ + ) +} diff --git a/apps/admin-web/src/components/ProductForm.tsx b/apps/admin-web/src/components/ProductForm.tsx new file mode 100644 index 0000000..c8f8610 --- /dev/null +++ b/apps/admin-web/src/components/ProductForm.tsx @@ -0,0 +1,659 @@ +import { useState, useImperativeHandle, forwardRef, useMemo } from 'react' +import { Formik, FieldArray } from 'formik' +import * as Yup from 'yup' +import { pInput as PInput, Dropdown, p as P, Checkbox, BottomDialog, ImageUploaderNeo } from 'web-components' +import type { ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'web-components' +import SearchableSelect from './SearchableSelect' +import { Plus, Trash2, X, RotateCcw, Info } from 'lucide-react' +import { trpc } from '@/lib/trpc-client' +import type { CreateComboItemInput, CreateSkuInput } from '@packages/shared' +import type { SkuFeatureLike } from '@packages/shared' + +// Feature input shape — single source: shared SkuFeatureLike +type Attribute = SkuFeatureLike + +// Variant form state — anchored to the shared CreateSkuInput (numeric prices, +// same field names). Images are managed separately via variantImages state. +type Variant = Omit< + CreateSkuInput, + 'name' | 'price' | 'marketPrice' | 'flashPrice' | 'images' | 'features' | 'comboItems' +> & { + id?: number + name: string + price?: number + marketPrice: number | null + flashPrice: number | null + isFlashAvailable: boolean + isOffer: boolean + isComboOnly: boolean + isSuspended: boolean + features: Attribute[] + comboItems: CreateComboItemInput[] +} + +interface ProductFormData { + name: string + shortDescription: string + longDescription: string + storeId: number + productType: 'item' | 'combo' + variants: Variant[] +} + +export interface ProductFormRef { + clearImages: () => void +} + +interface ProductFormProps { + mode: 'create' | 'edit' + initialValues: ProductFormData + onSubmit: (values: ProductFormData, variantImages: ImageUploaderNeoPayload[][], deletedImageKeys: string[]) => void + isLoading: boolean + existingVariantImages?: ImageUploaderNeoItem[][] + existingVariantImageKeys?: string[][] +} + +// Web rebuild of common-ui InfoDialog: info icon button + BottomDialog + p +function InfoDialog({ message, className }: { message?: string; className?: string }) { + const [open, setOpen] = useState(false) + return ( + <> + + setOpen(false)}> +
+

{message ?? ''}

+
+
+ + ) +} + +const defaultAttribute = (): Attribute => ({ featureName: '', featureValue: '' }) + +// The quantity feature is auto-added as the first attribute of every SKU. +const quantityAttribute = (): Attribute => ({ featureName: 'quantity', featureValue: '' }) + +const isQuantityFeature = (attr: Attribute): boolean => + (attr.featureName ?? '').trim().toLowerCase() === 'quantity' + +const defaultVariant = (): Variant => ({ + id: undefined, + name: '', + price: undefined, + marketPrice: null, + isFlashAvailable: false, + flashPrice: null, + isOffer: false, + isComboOnly: false, + isDeleted: false, + isSuspended: false, + features: [quantityAttribute()], + comboItems: [], +}) + +const variantSignature = (attributes: Attribute[]): string => + attributes + .map((a) => `${(a.featureName ?? '').trim().toLowerCase()}::${a.featureValue.trim().toLowerCase()}`) + .sort() + .join('|') + +const productValidationSchema = Yup.object().shape({ + name: Yup.string().required('Product name is required'), + storeId: Yup.number().required('Store is required').min(1, 'Store is required'), + productType: Yup.string().oneOf(['item', 'combo'], 'Product type is required').required('Product type is required'), + variants: Yup.array() + .min(1, 'At least one variant is required') + .of( + Yup.object().shape({ + price: Yup.number() + .typeError('Price must be a number') + .positive('Price must be a positive number') + .required('Price is required'), + marketPrice: Yup.number() + .typeError('Market price must be a number') + .min(0, 'Market price cannot be negative') + .nullable() + .transform((value, originalValue) => (originalValue === '' ? null : value)) + .optional(), + flashPrice: Yup.number() + .typeError('Flash price must be a number') + .min(0, 'Flash price cannot be negative') + .nullable() + .transform((value, originalValue) => (originalValue === '' ? null : value)) + .optional(), + features: Yup.array() + .min(1, 'At least one attribute is required') + .of( + Yup.object().shape({ + featureValue: Yup.string().required('Value is required'), + }) + ), + }) + ) + .test('unique-variants', 'Two variants have the same attributes', function (variants) { + if (!Array.isArray(variants)) return true + const seen = new Set() + for (const variant of variants) { + const attrs = variant?.features || [] + const signature = variantSignature(attrs as Attribute[]) + if (seen.has(signature)) { + return this.createError({ message: 'Two variants have the same attributes' }) + } + seen.add(signature) + } + return true + }) + .test('quantity-feature', 'Each SKU must have exactly one quantity feature', function (variants) { + if (!Array.isArray(variants)) return true + for (const variant of variants) { + const attrs = variant?.features || [] + const quantityCount = attrs.filter( + (a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity' + ).length + if (quantityCount === 0) { + return this.createError({ message: 'Every SKU must have a quantity feature' }) + } + if (quantityCount > 1) { + return this.createError({ message: 'Each SKU can have only one quantity feature' }) + } + } + return true + }), +}) + +// Collect every string message from a Formik errors tree (objects, arrays, strings). +// Variant-level errors (e.g. 'Each SKU can have only one quantity feature') can get +// hidden by Formik when index-level errors are also present, so we walk everything. +const collectErrorMessages = (errors: unknown, depth = 0): string[] => { + if (depth > 6) return [] + if (typeof errors === 'string') return [errors] + if (Array.isArray(errors)) return errors.flatMap((e) => collectErrorMessages(e, depth + 1)) + if (errors && typeof errors === 'object') { + return Object.values(errors).flatMap((e) => collectErrorMessages(e, depth + 1)) + } + return [] +} + +const ProductForm = forwardRef(({ + mode, + initialValues, + onSubmit, + isLoading, + existingVariantImages = [], + existingVariantImageKeys = [], +}, ref) => { + const [variantImages, setVariantImages] = useState(() => + initialValues.variants.length > 0 + ? initialValues.variants.map((_, i) => existingVariantImages[i] || []) + : [[]] + ) + + useImperativeHandle(ref, () => ({ + clearImages: () => setVariantImages(initialValues.variants.map(() => [])), + }), [initialValues.variants]) + + const { data: storesData } = trpc.common.getStoresSummary.useQuery() + const storeOptions = storesData?.stores.map((store) => ({ + label: store.name, + value: store.id, + })) || [] + + const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery() + const skuOptions = (skusData?.skus || []).map((sku) => ({ + label: sku.label, + value: sku.id, + })) + + // Make sure every SKU starts with a 'quantity' feature (auto-added at the + // beginning if the provided initial values don't have one). + const formInitialValues = useMemo(() => { + return { + ...initialValues, + variants: (initialValues.variants || []).map((v) => { + const hasQuantity = (v.features || []).some( + (a) => (a.featureName ?? '').trim().toLowerCase() === 'quantity' + ) + if (hasQuantity) return v + return { ...v, features: [quantityAttribute(), ...(v.features || [])] } + }), + } + }, [initialValues]) + + return ( + { + // Normalize feature names before sending to backend: the mandatory + // 'quantity' feature must always go out lowercase (case-insensitive + // matching means 'Quantity'/'QUANTITY' are treated the same), and + // names are trimmed like the backend does. + // TextInputs emit strings — coerce price fields to the numeric types + // the backend zod schema requires (price required; market/flash nullable). + const toNumber = (value: unknown): number | null => { + if (value == null || value === '') return null + const n = Number(value) + return Number.isFinite(n) ? n : null + } + const normalizedValues: ProductFormData = { + ...values, + variants: values.variants.map((v) => ({ + ...v, + price: toNumber(v.price) ?? undefined, + marketPrice: toNumber(v.marketPrice), + flashPrice: toNumber(v.flashPrice), + features: v.features.map((a) => ({ + ...a, + featureName: isQuantityFeature(a) ? 'quantity' : (a.featureName ?? '').trim() || null, + })), + })), + } + const images = variantImages.map((imgs) => + imgs.map((img) => ({ url: img.imgUrl, mimeType: img.mimeType })) + ) + const deletedKeys: string[] = [] + if (mode === 'edit') { + variantImages.forEach((currentImgs, vIndex) => { + const existing = existingVariantImages[vIndex] || [] + existing.forEach((existingImg) => { + if (!currentImgs.some((cur) => cur.imgUrl === existingImg.imgUrl)) { + const key = existingVariantImageKeys[vIndex]?.[existingVariantImages[vIndex]?.indexOf(existingImg)] + if (key) deletedKeys.push(key) + } + }) + }) + } + onSubmit(normalizedValues, images, deletedKeys) + }} + enableReinitialize + > + {({ handleChange, handleSubmit, values, setFieldValue, validateForm }) => { + return ( +
+ + + +
+

Store

+ setFieldValue('storeId', Number(value))} + className="w-full" + /> +
+ +
+

Product Type

+ setFieldValue('productType', value as 'item' | 'combo')} + className="w-full" + /> +
+ + + {({ push, remove }) => ( +
+
+

Variants

+ +
+ + {values.variants.map((variant, vIndex) => { + const isExistingSku = variant.id != null + const isMarkedDeleted = !!variant.isDeleted + return ( +
+
+
+

Variant {vIndex + 1}

+ {isMarkedDeleted && ( +
+

Deleted

+
+ )} +
+ {(values.variants.length > 1 || isExistingSku) && ( + + )} +
+ + + + + {({ push: pushAttr, remove: removeAttr }) => ( +
+
+

Attributes

+ +
+ {variant.features.map((attr, aIndex) => { + const quantityCount = variant.features.filter(isQuantityFeature).length + // Quantity features are locked (non-editable), but if there + // are duplicates (e.g. 'quantity' + 'Quantity') the user must + // be able to delete the extras to fix the form. + const canDelete = variant.features.length > 1 && (!isQuantityFeature(attr) || quantityCount > 1) + return ( +
+
+ + setFieldValue( + `variants.${vIndex}.features.${aIndex}.featureName`, + e.target.value.trim().toLowerCase() === 'quantity' ? 'quantity' : e.target.value + ) + } + disabled={isQuantityFeature(attr)} + className={isQuantityFeature(attr) ? 'text-gray-400' : undefined} + /> +
+
+ +
+ {canDelete && ( + + )} +
+ ) + })} +
+ + +
+
+ )} +
+ +
+
+ +
+
+ +
+
+ +
+ { + setFieldValue(`variants.${vIndex}.isFlashAvailable`, !variant.isFlashAvailable) + if (variant.isFlashAvailable) setFieldValue(`variants.${vIndex}.flashPrice`, null) + }} + /> +

Flash Available

+
+ +
+ setFieldValue(`variants.${vIndex}.isOffer`, !variant.isOffer)} + /> +

Offer SKU

+
+ + {!(values.productType === 'combo' && (variant.comboItems?.length || 0) > 0) && ( +
+ setFieldValue(`variants.${vIndex}.isComboOnly`, !variant.isComboOnly)} + /> +

Combo Only SKU

+
+ )} + + {mode === 'edit' && ( +
+ setFieldValue(`variants.${vIndex}.isSuspended`, !variant.isSuspended)} + /> +

Suspend SKU

+
+ )} + + {variant.isFlashAvailable && ( + + )} + + + setVariantImages((prev) => { + const next = [...prev] + next[vIndex] = [...(next[vIndex] || []), ...payloads.map((pl) => ({ imgUrl: pl.url, mimeType: pl.mimeType }))] + return next + }) + } + onImageRemove={(payload) => + setVariantImages((prev) => { + const next = [...prev] + next[vIndex] = (next[vIndex] || []).filter((img) => img.imgUrl !== payload.url) + return next + }) + } + allowMultiple={true} + /> + + {values.productType === 'combo' && ( +
+
+

Included Items

+ +
+ {variant.comboItems?.map((ci, cIndex) => ( +
+
+ ({ + ...opt, + // Disable SKUs already picked in another row of this combo. + disabled: (variant.comboItems || []).some( + (item, idx) => idx !== cIndex && String(item.skuId) === String(opt.value) + ), + }))} + onValueChange={(val) => { + const current = variant.comboItems || [] + const isDuplicate = current.some( + (item, idx) => idx !== cIndex && String(item.skuId) === String(val) + ) + if (isDuplicate) { + window.alert('Duplicate: This product is already in the combo') + return + } + setFieldValue(`variants.${vIndex}.comboItems.${cIndex}.skuId`, Number(val)) + }} + placeholder="Select SKU" + /> +
+ +
+ ))} +
+ )} +
+ ) + })} + + +
+ )} +
+ + +
+ ) + }} +
+ ) +}) + +ProductForm.displayName = 'ProductForm' + +export default ProductForm diff --git a/apps/admin-web/src/components/ProductGroupForm.tsx b/apps/admin-web/src/components/ProductGroupForm.tsx new file mode 100644 index 0000000..8ae88f7 --- /dev/null +++ b/apps/admin-web/src/components/ProductGroupForm.tsx @@ -0,0 +1,140 @@ +import { type FC } from 'react' +import { useFormik } from 'formik' +import { p as P, pInput as PInput } from 'web-components' +import ProductsSelector from './ProductsSelector' +import { trpc } from '@/lib/trpc-client' +import { X } from 'lucide-react' +import type { AdminProductGroup as SharedAdminProductGroup } from '@packages/shared' + +// Serialized product group (string date, loosely typed products) anchored to shared +export type ProductGroup = Omit & { + createdAt: string + products: any[] +} + +interface ProductGroupFormProps { + group?: ProductGroup | null + onClose: () => void + onSuccess: () => void +} + +const ProductGroupForm: FC = ({ + group, + onClose, + onSuccess, +}) => { + // Fetch products + const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery() + + const createGroup = trpc.admin.product.createGroup.useMutation() + const updateGroup = trpc.admin.product.updateGroup.useMutation() + + const isEditing = !!group + + const products = productsData?.products || [] + + const formik = useFormik({ + initialValues: { + group_name: group?.groupName || '', + description: group?.description || '', + product_ids: group?.products?.map(p => p.id) || [], + }, + validate: (values) => { + const errors: {[key: string]: string} = {} + + if (!values.group_name.trim()) { + errors.group_name = 'Group name is required' + } + + return errors + }, + onSubmit: async (values) => { + try { + if (isEditing) { + await updateGroup.mutateAsync({ + id: group.id, + group_name: values.group_name, + description: values.description, + product_ids: values.product_ids, + }) + } else { + await createGroup.mutateAsync({ + group_name: values.group_name, + description: values.description, + product_ids: values.product_ids, + }) + } + onSuccess() + } catch (error: any) { + window.alert(`Error: ${error.message || 'Failed to save group'}`) + } + }, + }) + + return ( +
+ {/* Header */} +
+

+ {isEditing ? 'Edit Product Group' : 'Create Product Group'} +

+ +
+ +
+ {/* Group Name */} + formik.setFieldValue('group_name', e.target.value)} + style={{ marginBottom: 16 }} + /> + {formik.errors.group_name && ( +

{formik.errors.group_name}

+ )} + + {/* Description */} + formik.setFieldValue('description', e.target.value)} + style={{ marginBottom: 16 }} + /> + + {/* Products Selection */} + formik.setFieldValue('product_ids', value as number[])} + multiple={true} + label="Products" + placeholder="Select products" + labelFormat={(product) => product.label} + /> + + {/* Actions */} +
+
+ Cancel +
+
formik.handleSubmit()} + className="flex-1 bg-brand-600 rounded-lg py-4 items-center text-center text-white font-medium cursor-pointer" + > + {createGroup.isPending || updateGroup.isPending ? 'Saving...' : 'Save'} +
+
+
+
+ ) +} + +export default ProductGroupForm diff --git a/apps/admin-web/src/components/ProductListDialog.tsx b/apps/admin-web/src/components/ProductListDialog.tsx new file mode 100644 index 0000000..a8071cc --- /dev/null +++ b/apps/admin-web/src/components/ProductListDialog.tsx @@ -0,0 +1,39 @@ +import type { FC } from 'react' +import { BottomDialog, p as P } from 'web-components' +import type { IdName } from '@packages/shared' + +type VendorSnippetProduct = IdName + +interface ProductListDialogProps { + open: boolean + onClose: () => void + products: VendorSnippetProduct[] +} + +export const ProductListDialog: FC = ({ + open, + onClose, + products, +}) => { + return ( + +
+

+ Products ({products.length}) +

+
+ {products.map((product, index) => ( +
+

+ {product.name} +

+
+ ))} +
+
+
+ ) +} diff --git a/apps/admin-web/src/components/ProductsSelector.tsx b/apps/admin-web/src/components/ProductsSelector.tsx new file mode 100644 index 0000000..e4e71c9 --- /dev/null +++ b/apps/admin-web/src/components/ProductsSelector.tsx @@ -0,0 +1,177 @@ +import React, { useState, useMemo } from 'react' +import { trpc } from '@/lib/trpc-client' +import type { SkuSummary as SharedSkuSummary } from '@packages/shared' +import MultiSelect from './MultiSelect' +import SearchableSelect from './SearchableSelect' + +// Selector works with skus that may not have images resolved yet +type SkuSummary = Omit + +interface Group { + id: number + groupName: string + products: { id: number }[] +} + +interface ProductsSelectorProps { + value: number | number[] + onChange: (value: number | number[]) => void + multiple?: boolean + label?: string + placeholder?: string + disabled?: boolean + error?: boolean + isDisabled?: (product: SkuSummary) => boolean + labelFormat?: (product: SkuSummary) => string + groups?: Group[] + selectedGroupIds?: number[] + onGroupChange?: (groupIds: number[]) => void + showGroups?: boolean +} + +export default function ProductsSelector({ + value, + onChange, + multiple = true, + label = 'Select Products', + placeholder = 'Select products', + disabled = false, + error = false, + isDisabled, + labelFormat, + groups = [], + showGroups = true, + selectedGroupIds = [], + onGroupChange, +}: ProductsSelectorProps) { + const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery() + const allSkus: SkuSummary[] = skusData?.skus || [] + const [searchQuery, setSearchQuery] = useState('') + + // Handle group selection changes + const handleGroupChange = (newGroupIds: number[]) => { + if (!onGroupChange) return + + const previousGroupIds = selectedGroupIds + + const addedGroups = newGroupIds.filter(id => !previousGroupIds.includes(id)) + const removedGroups = previousGroupIds.filter(id => !newGroupIds.includes(id)) + + let currentSkus = Array.isArray(value) ? [...value] : value ? [value] : [] + + const addedSkus = addedGroups.flatMap(groupId => { + const group = groups.find(g => g.id === groupId) + if (!group) return [] + const productIds = new Set(group.products.map(p => p.id)) + return allSkus.filter(s => productIds.has(s.productId)).map(s => s.id) + }) + + const removedSkus = removedGroups.flatMap(groupId => { + const group = groups.find(g => g.id === groupId) + if (!group) return [] + const productIds = new Set(group.products.map(p => p.id)) + return allSkus.filter(s => productIds.has(s.productId)).map(s => s.id) + }) + + currentSkus = [...new Set([...currentSkus, ...addedSkus])] + currentSkus = currentSkus.filter(id => !removedSkus.includes(id)) + + onGroupChange(newGroupIds) + if (multiple) { + onChange(currentSkus.length > 0 ? currentSkus : []) + } else { + onChange(currentSkus.length > 0 ? currentSkus[0] : 0) + } + } + + // Filter products based on search query + const filteredSkus = useMemo(() => { + if (!searchQuery.trim()) return allSkus + + const query = searchQuery.toLowerCase() + return allSkus.filter(sku => + sku.label.toLowerCase().includes(query) || + sku.productName.toLowerCase().includes(query) + ) + }, [allSkus, searchQuery]) + + // Build dropdown options + const productOptions = useMemo(() => { + return filteredSkus.map((sku) => { + const isFromGroup = selectedGroupIds.length > 0 && groups.some(group => { + if (!selectedGroupIds.includes(group.id)) return false + return group.products.some(p => p.id === sku.productId) + }) + + const isProductDisabled = isDisabled ? isDisabled(sku) : false + + const displayLabel = labelFormat + ? labelFormat(sku) + : sku.label + + return { + label: `${displayLabel}${isFromGroup ? ' (from group)' : ''}`, + value: sku.id.toString(), + disabled: isProductDisabled, + } + }) + }, [filteredSkus, selectedGroupIds, groups, isDisabled, labelFormat]) + + // Build group options if groups are provided + const groupOptions = useMemo(() => { + return groups.map(group => ({ + label: group.groupName, + value: group.id.toString(), + })) + }, [groups]) + + return ( +
+ {showGroups && groups.length > 0 && ( +
+ id.toString())} + onValueChange={(selectedValue) => { + const selectedValues = Array.isArray(selectedValue) ? selectedValue : typeof selectedValue === 'string' ? [selectedValue] : [] + const newGroupIds = selectedValues.map(v => parseInt(v as string)) + handleGroupChange(newGroupIds) + }} + placeholder="Select product groups (optional)" + /> +
+ )} + + {multiple ? ( + id.toString()) : []} + onValueChange={(selectedValue) => { + const selectedValues = Array.isArray(selectedValue) ? selectedValue : typeof selectedValue === 'string' ? [selectedValue] : [] + onChange(selectedValues.map(v => Number(v))) + }} + placeholder={placeholder} + disabled={disabled} + error={error} + onSearch={setSearchQuery} + /> + ) : ( + { + onChange(Number(selectedValue)) + }} + placeholder={placeholder} + disabled={disabled} + error={error} + onSearch={setSearchQuery} + /> + )} +
+ ) +} diff --git a/apps/admin-web/src/components/SearchableSelect.tsx b/apps/admin-web/src/components/SearchableSelect.tsx new file mode 100644 index 0000000..98ef9eb --- /dev/null +++ b/apps/admin-web/src/components/SearchableSelect.tsx @@ -0,0 +1,114 @@ +import { useMemo, useState } from 'react' +import { BottomDialog, SearchBar, p as P } from 'web-components' +import { Check, ChevronDown } from 'lucide-react' + +export interface SearchableSelectOption { + label: string + value: string | number + disabled?: boolean +} + +interface SearchableSelectProps { + label: string + options: SearchableSelectOption[] + value: string | number + onValueChange: (v: string | number) => void + onSearch?: (q: string) => void + placeholder?: string + disabled?: boolean + error?: boolean + className?: string + testID?: string + // Tolerated for screen compatibility (RN BottomDropdown leftover) — ignored on web + triggerComponent?: any +} + +export function SearchableSelect({ + label, + options, + value, + onValueChange, + onSearch, + placeholder, + disabled = false, + error = false, + className, + testID, +}: SearchableSelectProps) { + const [open, setOpen] = useState(false) + const [query, setQuery] = useState('') + + const selectedLabel = useMemo(() => { + const found = options.find((o) => String(o.value) === String(value)) + return found ? found.label : null + }, [options, value]) + + const displayText = selectedLabel ?? placeholder ?? label + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase() + if (!q) return options + return options.filter((o) => o.label.toLowerCase().includes(q)) + }, [options, query]) + + const handleSelect = (optionValue: string | number) => { + onValueChange(optionValue) + setOpen(false) + } + + return ( +
+

{label}

+ + setOpen(false)}> +
+

{label}

+ { + setQuery(q) + onSearch?.(q) + }} + onSearch={onSearch} + className="mb-3" + /> +
+ {filtered.map((option) => { + const selected = String(option.value) === String(value) + return ( + + ) + })} + {filtered.length === 0 ? ( +

No options found

+ ) : null} +
+
+
+
+ ) +} + +export default SearchableSelect diff --git a/apps/admin-web/src/components/SlotForm.tsx b/apps/admin-web/src/components/SlotForm.tsx new file mode 100644 index 0000000..18a7537 --- /dev/null +++ b/apps/admin-web/src/components/SlotForm.tsx @@ -0,0 +1,229 @@ +import React from 'react' +import type { SlotSnippetInput } from '@packages/shared' +import { Formik, FieldArray } from 'formik' +import DateTimeInput from './DateTimeInput' +import { p as P, pInput as PInput } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import ProductsSelector from './ProductsSelector' + +// Snippet form row — single source in @packages/shared (complete payload) +type VendorSnippet = SlotSnippetInput + +interface SlotFormProps { + onSlotAdded?: () => void + initialDeliveryTime?: Date | null + initialFreezeTime?: Date | null + initialIsActive?: boolean + slotId?: number + initialProductIds?: number[] + initialGroupIds?: number[] +} + +export default function SlotForm({ + onSlotAdded, + initialDeliveryTime, + initialFreezeTime, + initialIsActive = true, + slotId, + initialProductIds = [], + initialGroupIds = [], +}: SlotFormProps) { + const { data: slotData } = trpc.admin.slots.getSlotById.useQuery( + { id: slotId! }, + { enabled: !!slotId } + ) + + const vendorSnippetsFromSlot = (slotData?.slot?.vendorSnippets || []).map((snippet: any) => ({ + name: snippet.name || '', + groupIds: snippet.groupIds || [], + skuIds: snippet.skuIds || [], + validTill: snippet.validTill || null, + })) as VendorSnippet[] + + const initialValues = { + deliveryTime: initialDeliveryTime || (slotData?.slot?.deliveryTime ? new Date(slotData.slot.deliveryTime) : null), + freezeTime: initialFreezeTime || (slotData?.slot?.freezeTime ? new Date(slotData.slot.freezeTime) : null), + selectedGroupIds: initialGroupIds.length > 0 ? initialGroupIds : (slotData?.slot?.groupIds || []), + selectedSkuIds: initialProductIds.length > 0 ? initialProductIds : (slotData?.slot?.products?.map((p: any) => p.id) || []), + vendorSnippetList: vendorSnippetsFromSlot, + } + + const { mutate: createSlot, isPending: isCreating } = trpc.admin.slots.createSlot.useMutation() + const { mutate: updateSlot, isPending: isUpdating } = trpc.admin.slots.updateSlot.useMutation() + + const isEditMode = !!slotId + const isPending = isCreating || isUpdating + + const { data: groupsData } = trpc.admin.product.getGroups.useQuery() + + const handleFormSubmit = (values: typeof initialValues) => { + if (!values.deliveryTime || !values.freezeTime) { + window.alert('Error: Please fill all fields') + return + } + + if (values.freezeTime > values.deliveryTime) { + window.alert('Error: Freeze time must be before or equal to delivery time') + return + } + + const slotInputData = { + deliveryTime: values.deliveryTime.toISOString(), + freezeTime: values.freezeTime.toISOString(), + isActive: initialIsActive, + groupIds: values.selectedGroupIds, + skuIds: values.selectedSkuIds, + vendorSnippets: values.vendorSnippetList.map((snippet: VendorSnippet) => ({ + name: snippet.name, + skuIds: snippet.skuIds, + validTill: snippet.validTill ?? undefined, + })), + } + + if (isEditMode && slotId) { + updateSlot( + { id: slotId, ...slotInputData }, + { + onSuccess: () => { + window.alert('Success: Slot updated successfully!') + onSlotAdded?.() + }, + onError: (error: any) => { + console.log({msg: JSON.stringify(error.message)}) + + window.alert(`Error: ${error.message || 'Failed to update slot'}`) + }, + } + ) + } else { + createSlot( + slotInputData, + { + onSuccess: () => { + window.alert('Success: Slot created successfully!') + onSlotAdded?.() + }, + onError: (error: any) => { + window.alert(`Error: ${error.message || 'Failed to create slot'}`) + }, + } + ) + } + } + + return ( + + {({ handleSubmit, values, setFieldValue }) => { + const mappedGroups = (groupsData?.groups || []).map(group => ({ + ...group, + products: group.products.map(product => ({ + ...product, + id: product.id, + })), + })) + + return ( +
+

+ {isEditMode ? 'Edit Slot' : 'Create New Slot'} +

+ +
+

Delivery Date & Time

+ setFieldValue('deliveryTime', value)} + /> +
+ +
+

Freeze Date & Time

+ setFieldValue('freezeTime', value)} + /> +
+ +
+ setFieldValue('selectedSkuIds', newSkuIds)} + groups={mappedGroups} + selectedGroupIds={values.selectedGroupIds} + onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)} + label="Select Products" + placeholder="Select products for this slot" + /> +
+ + {/* Vendor Snippets */} + + {({ push, remove }) => ( +
+

Vendor Snippets

+ {values.vendorSnippetList.map((snippet: VendorSnippet, index: number) => ( +
+
+ setFieldValue(`vendorSnippetList.${index}.name`, e.target.value)} + /> +
+ +
+ setFieldValue(`vendorSnippetList.${index}.skuIds`, newSkuIds)} + groups={mappedGroups.filter(group => + values.selectedGroupIds.includes(group.id) + ).map(group => ({ + ...group, + products: group.products.filter((prod: any) => values.selectedSkuIds.includes(prod.id)) + }))} + selectedGroupIds={snippet.groupIds || []} + onGroupChange={(newGroupIds) => setFieldValue(`vendorSnippetList.${index}.groupIds`, newGroupIds)} + label="Select Products" + placeholder="Select products for snippet" + isDisabled={(sku) => !values.selectedSkuIds.includes(sku.id)} + /> +
+ +
+ ))} + +
+ )} +
+ + +
+ )}} +
+ ) +} diff --git a/apps/admin-web/src/components/SnippetMenu.tsx b/apps/admin-web/src/components/SnippetMenu.tsx new file mode 100644 index 0000000..893f93d --- /dev/null +++ b/apps/admin-web/src/components/SnippetMenu.tsx @@ -0,0 +1,156 @@ +import { useState, type FC } from 'react' +import { p as P, BottomDialog } from 'web-components' +import { SuccessToast } from '@/services/toaster' +import type { VendorSnippet } from '@/types/vendor-snippets' +import { Eye, Pencil, Link, Trash2, MoreVertical } from 'lucide-react' +import type { CSSProperties } from 'react' + +export interface SnippetMenuOption { + title: string + icon: 'eye' | 'edit' | 'link' | 'trash' + onPress: () => void + disabled?: boolean +} + +export interface SnippetMenuProps { + snippet: VendorSnippet + onEdit: (snippet: VendorSnippet) => void + onDelete: (id: number) => void + onViewOrders: (snippetCode: string) => void + triggerStyle?: any + iconSize?: number + iconColor?: string +} + +const iconMap = { + eye: Eye, + edit: Pencil, + link: Link, + trash: Trash2, +} + +export const SnippetMenu: FC = ({ + snippet, + onEdit, + onDelete, + onViewOrders, + triggerStyle = 'p-2 rounded-full bg-gray-50', + iconSize = 16, + iconColor = '#6B7280', +}) => { + const [isOpen, setIsOpen] = useState(false) + + const handleOpenMenu = () => { + setIsOpen(true) + } + + const handleCloseMenu = () => { + setIsOpen(false) + } + + const handleViewOrders = () => { + setIsOpen(false) + onViewOrders(snippet.snippetCode) + } + + const handleEdit = () => { + setIsOpen(false) + onEdit(snippet) + } + + const handleDelete = () => { + setIsOpen(false) + const confirmed = window.confirm( + 'Delete Snippet? Are you sure you want to delete this vendor snippet?' + ) + if (confirmed) { + onDelete(snippet.id) + } + } + + const handleCopyUrl = async () => { + setIsOpen(false) + if (!snippet.accessUrl) { + window.alert('Error: No URL available to copy') + return + } + await navigator.clipboard.writeText(snippet.accessUrl) + SuccessToast('URL copied to clipboard') + } + + const options: SnippetMenuOption[] = [ + { + title: 'View Orders', + icon: 'eye', + onPress: handleViewOrders, + }, + { + title: 'Edit', + icon: 'edit', + onPress: handleEdit, + }, + { + title: 'Copy URL', + icon: 'link', + onPress: handleCopyUrl, + }, + { + title: 'Delete', + icon: 'trash', + onPress: handleDelete, + }, + ] + + const handleOptionPress = (option: SnippetMenuOption) => { + if (option.disabled) return + option.onPress() + } + + const triggerClassName = typeof triggerStyle === 'string' ? triggerStyle : 'p-2 rounded-full bg-gray-50' + const triggerStyleObj: CSSProperties | undefined = typeof triggerStyle === 'object' ? triggerStyle : undefined + + return ( + <> + {/* Menu Trigger */} + + + {/* Menu Dialog */} + +
+

+ Snippet Options +

+ + {options.map((option, index) => { + const Icon = iconMap[option.icon] + return ( + + ) + })} +
+
+ + ) +} diff --git a/apps/admin-web/src/components/SnippetOrdersView.tsx b/apps/admin-web/src/components/SnippetOrdersView.tsx new file mode 100644 index 0000000..6186aa2 --- /dev/null +++ b/apps/admin-web/src/components/SnippetOrdersView.tsx @@ -0,0 +1,156 @@ +import { Fragment, type FC } from 'react' +import { p as P, AppContainer } from 'web-components' +import { ShoppingCart } from 'lucide-react' +import type { + AdminVendorSnippetOrderProduct, + AdminVendorSnippetOrderSummary as SharedSnippetOrderSummary, +} from '@packages/shared' + +// Line item + order summary anchored to the shared vendor-order contract +// (string totalAmount + any[] sequence at this view layer) +type OrderProduct = Pick< + AdminVendorSnippetOrderProduct, + 'productId' | 'productName' | 'quantity' | 'price' | 'unit' | 'subtotal' +> + +type SnippetOrder = Omit & { + totalAmount: string + slotInfo: { + time: string + sequence: any[] + } | null + products: OrderProduct[] +} + +interface SnippetOrdersViewProps { + orders: SnippetOrder[] + snippetCode: string + onClose: () => void +} + +const OrderCard: FC<{ order: SnippetOrder; index: number }> = ({ order, index }) => { + const orderDate = new Date(order.orderDate) + const slotTime = order.slotInfo ? new Date(order.slotInfo.time) : null + + return ( +
+ {/* Order Header */} +
+
+

{order.orderId}

+

{order.customerName}

+
+
+

₹{order.totalAmount}

+

+ {orderDate.toLocaleDateString()} +

+
+
+ + {/* Slot Info */} + {slotTime && ( +
+

+ Delivery: {slotTime.toLocaleString()} +

+
+ )} + + {/* Products */} +
+

Products:

+ {order.products.map((product, index) => { + const isMatched = order.matchedProducts.includes(product.productId) + return ( +
+
+

+ {product.productName} + {isMatched && ' ⭐'} +

+

+ {product.quantity} {product.unit} × ₹{product.price} +

+
+

+ ₹{product.subtotal.toFixed(2)} +

+
+ ) + })} +
+ + {/* Matched Products Summary */} +
+

+ Matched {order.matchedProducts.length} product(s) from snippet "{order.snippetCode}" +

+
+
+ ) +} + +const SnippetOrdersView: FC = ({ + orders, + snippetCode, + onClose, +}) => { + const totalOrders = orders.length + const totalRevenue = orders.reduce((sum, order) => sum + parseFloat(order.totalAmount), 0) + + return ( + +
+ {/* Header */} +
+
+
+

Orders for "{snippetCode}"

+

+ {totalOrders} order{totalOrders !== 1 ? 's' : ''} • Total: ₹{totalRevenue.toFixed(2)} +

+
+ +
+
+ + {/* Orders List */} +
+ {orders.length === 0 ? ( +
+ +

No matching orders

+

+ No orders found that match this snippet's criteria +

+
+ ) : ( +
+ {orders.map((order, index) => ( + + + {index < orders.length - 1 && ( +
+ )} + + ))} +
+ )} +
+
+ + ) +} + +export default SnippetOrdersView diff --git a/apps/admin-web/src/components/StoreForm.tsx b/apps/admin-web/src/components/StoreForm.tsx new file mode 100644 index 0000000..e44870b --- /dev/null +++ b/apps/admin-web/src/components/StoreForm.tsx @@ -0,0 +1,197 @@ +import { useState, useEffect, useMemo, useRef, type ChangeEvent } from 'react' +import { Formik } from 'formik' +import * as Yup from 'yup' +import { pInput as PInput, Dropdown, p as P, ImageUploader } from 'web-components' +import ProductsSelector from './ProductsSelector' +import { trpc } from '@/lib/trpc-client' +import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStorage' +import type { CreateStoreInput } from '@packages/shared' + +// Store form values — required description + product selection on top of CreateStoreInput +export type StoreFormData = Omit & { + description: string + products: number[] +} + +interface StoreFormProps { + mode: 'create' | 'edit' + initialValues: StoreFormData + onSubmit: (values: StoreFormData) => void + isLoading: boolean + storeId?: number +} + +const validationSchema = Yup.object().shape({ + name: Yup.string().required('Name is required'), + description: Yup.string(), + imageUrl: Yup.string(), + owner: Yup.number().required('Owner is required'), + products: Yup.array().of(Yup.number()), +}) + +function StoreForm({ mode, initialValues, onSubmit, isLoading, storeId }: StoreFormProps) { + const { data: staffData } = trpc.admin.staffUser.getStaff.useQuery() + const { data: productsData } = trpc.admin.product.getProducts.useQuery() + + const [formInitialValues, setFormInitialValues] = useState(initialValues) + const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]) + const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]) + const fileInputRef = useRef(null) + + // For edit mode, pre-select SKUs belonging to this store's products + const initialSelectedProducts = useMemo(() => { + if (mode !== 'edit' || !productsData?.products) return [] + return productsData.products + .filter(p => p.storeId === storeId) + .flatMap(p => (p.skus || []).map(sku => sku.id)) + }, [mode, productsData?.products, storeId]) + + useEffect(() => { + setFormInitialValues({ + ...initialValues, + products: initialSelectedProducts, + }) + }, [initialValues, initialSelectedProducts]) + + const existingImageUrls = useMemo( + () => (formInitialValues.imageUrl ? [formInitialValues.imageUrl] : []), + [formInitialValues.imageUrl] + ) + + const staffOptions = staffData?.staff.map((staff: { id: number; name: string }) => ({ + label: staff.name, + value: staff.id, + })) || [] + + const { uploadSingle, isUploading } = useUploadToObjectStorage() + + const handleImagePick = () => { + fileInputRef.current?.click() + } + + const handleFileChange = (e: ChangeEvent) => { + const files = Array.from(e.target.files || []) + if (files.length === 0) { + setSelectedImages([]) + setDisplayImages([]) + return + } + const file = files[0] + setSelectedImages([{ blob: file, mimeType: file.type || 'image/jpeg' }]) + setDisplayImages([{ uri: URL.createObjectURL(file) }]) + e.target.value = '' + } + + const handleRemoveImage = (uri: string) => { + const index = displayImages.findIndex(img => img.uri === uri) + if (index !== -1) { + const newDisplay = displayImages.filter((_, i) => i !== index) + const newFiles = selectedImages.filter((_, i) => i !== index) + setDisplayImages(newDisplay) + setSelectedImages(newFiles) + } + } + + return ( + + {({ handleChange, handleSubmit, values, setFieldValue, errors, touched }) => { + const submit = async () => { + try { + let imageUrl: string | undefined + + if (selectedImages.length > 0) { + const { blob, mimeType } = selectedImages[0] + const { presignedUrl } = await uploadSingle(blob, mimeType, 'store') + imageUrl = presignedUrl + } + + onSubmit({ ...values, imageUrl }) + } catch (error) { + console.error('Upload error:', error) + window.alert('Error: Failed to upload image') + } + } + + return ( +
+ + + +
+

Owner

+ setFieldValue('owner', Number(value))} + className="mb-4" + /> +
+ setFieldValue('products', value)} + multiple={true} + label="Products" + placeholder="Select products" + isDisabled={(sku) => sku.storeId !== null && sku.storeId !== storeId} + labelFormat={(sku) => `${sku.productName} - ₹${sku.price}`} + /> +
+

Store Image

+ + setFormInitialValues((prev) => ({ + ...prev, + imageUrl: undefined, + })) + } + allowMultiple={false} + /> +
+ +
+ ) + }} +
+ ) +} + +export default StoreForm diff --git a/apps/admin-web/src/components/TagForm.tsx b/apps/admin-web/src/components/TagForm.tsx new file mode 100644 index 0000000..356deb3 --- /dev/null +++ b/apps/admin-web/src/components/TagForm.tsx @@ -0,0 +1,345 @@ +import { useState, useEffect, forwardRef, useCallback, type CSSProperties } from 'react' +import { Formik } from 'formik' +import * as Yup from 'yup' +import { pInput as PInput, p as P, Checkbox, ImageUploaderNeo } from 'web-components' +import type { ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'web-components' +import MultiSelect from './MultiSelect' +import { GripVertical, X, Package, Image as ImageIcon } from 'lucide-react' +import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' +import type { DragEndEvent } from '@dnd-kit/core' +import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import ProductsSelector from './ProductsSelector' +import { trpc } from '@/lib/trpc-client' +import useFocusCallback from '@/lib/refresh-context' +import type { IdName } from '@packages/shared' + +type StoreOption = IdName + +export interface TagFormData { + tagName: string + tagDescription: string + isDashboardTag: boolean + relatedStores: number[] + productIds: number[] +} + +interface TagFormProps { + mode: 'create' | 'edit' + initialValues: TagFormData + existingImageUrl?: string + onSubmit: (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => void + isLoading: boolean + stores?: StoreOption[] +} + +interface SelectedProduct { + skuId: number + productId: number + label: string + imageUrl?: string | null +} + +const itemStyle: CSSProperties = { + backgroundColor: 'white', + borderRadius: 8, + borderWidth: 1, + borderColor: '#e5e7eb', + borderStyle: 'solid', + padding: 10, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + boxShadow: '0 1px 2px rgba(0, 0, 0, 0.1)', + marginTop: 4, + marginBottom: 4, +} + +function SortableProductRow({ item, onRemove }: { item: SelectedProduct; onRemove: (productId: number) => void }) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.productId }) + const style: CSSProperties = { + ...itemStyle, + transform: CSS.Transform.toString(transform), + transition, + ...(isDragging + ? { borderColor: '#3b82f6', boxShadow: '0 4px 8px rgba(59, 130, 246, 0.3)' } + : {}), + } + return ( +
+ + {item.imageUrl ? ( + {item.label} + ) : ( +
+ +
+ )} +

+ {item.label} +

+ +
+ ) +} + +const TagForm = forwardRef(({ + mode, + initialValues, + existingImageUrl: existingImageUrlRaw, + onSubmit, + isLoading, + stores: storesRaw, +}, ref) => { + const [images, setImages] = useState([]) + const [removedExisting, setRemovedExisting] = useState(false) + const [isDashboardTagChecked, setIsDashboardTagChecked] = useState(Boolean(initialValues.isDashboardTag)) + const [selectedProducts, setSelectedProducts] = useState([]) + const sensors = useSensors(useSensor(PointerSensor)) + + const existingImageUrl = existingImageUrlRaw || '' + const stores = storesRaw || [] + + const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery() + const allSkus: SelectedProduct[] = (skusData?.skus || []).map((sku: any) => ({ + skuId: sku.id, + productId: sku.productId, + label: sku.label, + imageUrl: sku.images?.[0] || null, + })) + + // Build the ordered list from initialValues.productIds (product ids, in sortOrder). + useEffect(() => { + const ordered: SelectedProduct[] = [] + const skuMap = new Map() + for (const sku of allSkus) { + skuMap.set(sku.productId, sku) + } + for (const productId of initialValues.productIds || []) { + const sku = skuMap.get(productId) + if (sku) ordered.push(sku) + else ordered.push({ skuId: 0, productId, label: `Product #${productId}`, imageUrl: null }) + } + setSelectedProducts(ordered) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialValues.productIds, skusData]) + + // Update checkbox when initial values change + useEffect(() => { + setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag)) + if (existingImageUrl) { + setImages([{ imgUrl: existingImageUrl, mimeType: null }]) + } else { + setImages([]) + } + setRemovedExisting(false) + }, [existingImageUrlRaw, initialValues.isDashboardTag]) + + const validationSchema = Yup.object().shape({ + tagName: Yup.string() + .required('Tag name is required') + .min(1, 'Tag name must be at least 1 character') + .max(100, 'Tag name must be less than 100 characters'), + tagDescription: Yup.string() + .max(500, 'Description must be less than 500 characters'), + }) + + // When a product is picked in the selector, add it to the ordered list (if not already present). + const handleProductSelect = (value: number | number[]) => { + const skuIds = Array.isArray(value) ? value : [value] + setSelectedProducts((prev) => { + const next = [...prev] + for (const skuId of skuIds) { + if (next.some((prod) => prod.skuId === skuId)) continue + const sku = allSkus.find((s) => s.skuId === skuId) + if (sku) next.push(sku) + } + return next + }) + } + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event + if (!over || active.id === over.id) return + setSelectedProducts((prev) => { + const oldIndex = prev.findIndex((prod) => prod.productId === active.id) + const newIndex = prev.findIndex((prod) => prod.productId === over.id) + if (oldIndex === -1 || newIndex === -1) return prev + return arrayMove(prev, oldIndex, newIndex) + }) + }, []) + + const handleRemoveProduct = useCallback((productId: number) => { + setSelectedProducts((prev) => prev.filter((prod) => prod.productId !== productId)) + }, []) + + return ( + onSubmit({ ...values, productIds: selectedProducts.map((prod) => prod.productId) }, images, removedExisting)} + enableReinitialize + > + {({ handleChange, handleSubmit, values, setFieldValue, errors, touched, resetForm }) => { + // Clear form when screen comes into focus (create mode only — edit keeps its loaded products) + const clearForm = useCallback(() => { + setImages([]) + setRemovedExisting(false) + setIsDashboardTagChecked(false) + if (mode === 'create') { + setSelectedProducts([]) + } + resetForm() + }, [resetForm, mode]) + + useFocusCallback(clearForm) + + return ( +
+ + {errors.tagName && touched.tagName ? ( +

{errors.tagName}

+ ) : null} + + + {errors.tagDescription && touched.tagDescription ? ( +

{errors.tagDescription}

+ ) : null} + + {/* Image Upload Section */} +
+

+ Tag Image {mode === 'edit' ? '(Upload new to replace)' : '(Optional)'} +

+ + { + setImages((prev) => [...prev, ...payload.map((img) => ({ + imgUrl: img.url, + mimeType: img.mimeType, + }))]) + }} + onImageRemove={(payload) => { + if (payload.mimeType === null) { + setRemovedExisting(true) + } + setImages((prev) => prev.filter((item) => item.imgUrl !== payload.url)) + }} + allowMultiple={false} + /> +
+ + {/* Dashboard Tag Checkbox */} +
+ { + const newValue = !isDashboardTagChecked + setIsDashboardTagChecked(newValue) + setFieldValue('isDashboardTag', newValue) + }} + /> +

Mark as Dashboard Tag

+
+ + {/* Related Stores Dropdown */} +
+

+ Related Stores +

+ id.toString())} + options={stores.map(store => ({ + label: store.name, + value: store.id.toString(), + }))} + onValueChange={(selectedValues) => { + const numericValues = (selectedValues as string[]).map(v => parseInt(v)) + setFieldValue('relatedStores', numericValues) + }} + /> +
+ + {/* Products Section: selector + reorderable list */} +
+

+ Products +

+ +
+ {selectedProducts.length > 0 && ( +
+

+ Drag to reorder • {selectedProducts.length} items +

+
+ )} + {selectedProducts.length > 0 ? ( + + prod.productId)} strategy={verticalListSortingStrategy}> + {selectedProducts.map((prod) => ( + + ))} + + + ) : ( +
+ +

No products selected yet

+
+ )} +
+
+ + +
+ ) + }} +
+ ) +}) + +TagForm.displayName = 'TagForm' + +export default TagForm diff --git a/apps/admin-web/src/components/TagMenu.tsx b/apps/admin-web/src/components/TagMenu.tsx new file mode 100644 index 0000000..62c93fa --- /dev/null +++ b/apps/admin-web/src/components/TagMenu.tsx @@ -0,0 +1,122 @@ +import { useState, type FC } from 'react' +import { p as P, BottomDialog } from 'web-components' +import { useNavigate } from '@tanstack/react-router' +import { trpc } from '@/lib/trpc-client' +import { Pencil, Trash2, MoreVertical } from 'lucide-react' +import type { CSSProperties } from 'react' + +export interface TagMenuProps { + tagId: number + onDeleteSuccess?: () => void + triggerStyle?: any + iconSize?: number + iconColor?: string +} + +export const TagMenu: FC = ({ + tagId, + onDeleteSuccess, + triggerStyle = 'p-2 rounded-full bg-gray-50', + iconSize = 16, + iconColor = '#6B7280', +}) => { + const [isOpen, setIsOpen] = useState(false) + const navigate = useNavigate() + const deleteTag = trpc.admin.product.deleteProductTag.useMutation() + + const handleOpenMenu = () => { + setIsOpen(true) + } + + const handleCloseMenu = () => { + setIsOpen(false) + } + + const handleEditTag = () => { + setIsOpen(false) + navigate({ to: '/dashboard/product-tags/edit', search: { tagId: String(tagId) } }) + } + + const handleDeleteTag = () => { + setIsOpen(false) + const confirmed = window.confirm( + 'Delete Tag? Are you sure you want to delete this tag? This action cannot be undone.' + ) + if (confirmed) { + performDelete() + } + } + + const performDelete = () => { + deleteTag.mutate({ id: tagId }, { + onSuccess: () => { + window.alert('Success: Tag deleted successfully') + onDeleteSuccess?.() + }, + onError: (error: any) => { + const errorMessage = error.message || 'Failed to delete tag' + window.alert(`Error: ${errorMessage}`) + }, + }) + } + + const options = [ + { + title: 'Edit Tag', + icon: Pencil, + onPress: handleEditTag, + }, + { + title: 'Delete Tag', + icon: Trash2, + onPress: handleDeleteTag, + }, + ] + + const triggerClassName = typeof triggerStyle === 'string' ? triggerStyle : 'p-2 rounded-full bg-gray-50' + const triggerStyleObj: CSSProperties | undefined = typeof triggerStyle === 'object' ? triggerStyle : undefined + + return ( + <> + {/* Menu Trigger */} + + + {/* Menu Dialog */} + +
+

+ Tag Options +

+ + {options.map((option, index) => { + const Icon = option.icon + return ( + + ) + })} +
+
+ + ) +} diff --git a/apps/admin-web/src/components/UserIncidentsView.tsx b/apps/admin-web/src/components/UserIncidentsView.tsx new file mode 100644 index 0000000..1c60982 --- /dev/null +++ b/apps/admin-web/src/components/UserIncidentsView.tsx @@ -0,0 +1,207 @@ +import React, { useState } from 'react' +import { p as P, BottomDialog, pInput as PInput } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import { TriangleAlert, Info, Plus, Calendar, User, ShoppingCart, CircleCheck } from 'lucide-react' +import dayjs from 'dayjs' + +function UserIncidentDialog({ + userId, + orderId, + open, + onClose, + onSuccess +}: { + userId: number + orderId: number | null + open: boolean + onClose: () => void + onSuccess?: () => void +}) { + const [adminComment, setAdminComment] = useState('') + const [negativityScore, setNegativityScore] = useState('') + + const addIncidentMutation = trpc.admin.user.addUserIncident.useMutation({ + onSuccess: () => { + window.alert('Success: Incident added successfully') + setAdminComment('') + setNegativityScore('') + onClose() + onSuccess?.() + }, + onError: (error: any) => { + window.alert(`Error: ${error.message || 'Failed to add incident'}`) + }, + }) + + const handleAddIncident = () => { + const score = negativityScore ? parseInt(negativityScore) : undefined + + if (!adminComment.trim() && !negativityScore) { + window.alert('Error: Please enter a comment or negativity score') + return + } + + addIncidentMutation.mutate({ + userId, + orderId: orderId || undefined, + adminComment: adminComment || undefined, + negativityScore: score, + }) + } + + return ( + +
+
+
+ +
+

+ Add User Incident +

+

+ Record an incident for this user. This will be visible in their profile. +

+
+ + setAdminComment(e.target.value)} + placeholder="Enter details about the incident..." + multiline + className="h-24" + /> + + setNegativityScore(e.target.value)} + placeholder="0" + type="number" + inputMode="numeric" + className="mt-4" + /> + +
+ +

+ Higher negativity scores indicate more serious incidents (e.g., repeated cancellations, abusive behavior). +

+
+ +
+ + +
+
+
+ ) +} + +export function UserIncidentsView({ userId, orderId }: { userId: number; orderId: number | null }) { + const [incidentDialogOpen, setIncidentDialogOpen] = useState(false) + + const { data: incidentsData, refetch: refetchIncidents } = trpc.admin.user.getUserIncidents.useQuery( + { userId }, + { enabled: !!userId } + ) + + return ( + <> +
+
+

+ User Incidents +

+ +
+ + {incidentsData?.incidents && incidentsData.incidents.length > 0 ? ( +
+ {incidentsData.incidents.map((incident: any, index: number) => ( +
+
+
+ +

+ {dayjs(incident.dateAdded).format('MMM DD, YYYY • h:mm A')} +

+
+ {incident.negativityScore && ( +
+

+ Score: {incident.negativityScore} +

+
+ )} +
+ + {incident.adminComment && ( +
+

+ {incident.adminComment} +

+
+ )} + +
+ +

+ Added by {incident.addedBy} +

+ {incident.orderId && ( + <> + +

+ Order #{incident.orderId} +

+ + )} +
+
+ ))} +
+ ) : ( +
+ +

+ No incidents recorded for this user +

+
+ )} +
+ + setIncidentDialogOpen(false)} + onSuccess={refetchIncidents} + /> + + ) +} diff --git a/apps/admin-web/src/components/VendorSnippetForm.tsx b/apps/admin-web/src/components/VendorSnippetForm.tsx new file mode 100644 index 0000000..fc0d73a --- /dev/null +++ b/apps/admin-web/src/components/VendorSnippetForm.tsx @@ -0,0 +1,225 @@ +import { useEffect, type FC } from 'react' +import { useFormik } from 'formik' +import { p as P, Dropdown, pInput as PInput, Checkbox } from 'web-components' +import ProductsSelector from './ProductsSelector' +import DateInput from './DateInput' +import { trpc } from '@/lib/trpc-client' +import type { VendorSnippetForm as VendorSnippetFormType } from '@/types/vendor-snippets' + +interface VendorSnippetFormProps { + snippet?: VendorSnippetFormType | null + onClose: () => void + onSuccess: () => void + showIsPermanentCheckbox?: boolean +} + +const VendorSnippetForm: FC = ({ + snippet, + onClose, + onSuccess, + showIsPermanentCheckbox = false, +}) => { + + // Fetch slots + const { data: slotsData } = trpc.user.slots.getSlots.useQuery() + + const createSnippet = trpc.admin.vendorSnippets.create.useMutation() + const updateSnippet = trpc.admin.vendorSnippets.update.useMutation() + + const isEditing = !!snippet + + const formik = useFormik({ + initialValues: { + snippetCode: snippet?.snippetCode || '', + slotId: snippet?.slotId?.toString() || '', + isPermanent: snippet?.isPermanent || false, + skuIds: snippet?.skuIds?.map(id => id.toString()) || [], + validTill: snippet?.validTill ? new Date(snippet.validTill) : null, + }, + validate: (values) => { + const errors: {[key: string]: string} = {} + + if (!values.snippetCode.trim()) { + errors.snippetCode = 'Snippet code is required' + } + + // Validate snippet code only contains alphanumeric characters and underscore + const snippetCodeRegex = /^[a-zA-Z0-9_]+$/ + if (values.snippetCode && !snippetCodeRegex.test(values.snippetCode)) { + errors.snippetCode = 'Snippet code can only contain letters, numbers, and underscores' + } + + // Only require slotId if isPermanent is false + if (!values.isPermanent && !values.slotId) { + errors.slotId = 'Slot selection is required' + } + + if (values.skuIds.length === 0) { + errors.skuIds = 'At least one product must be selected' + } + + return errors + }, + onSubmit: async (values) => { + try { + + const submitData = { + snippetCode: values.snippetCode, + slotId: values.isPermanent ? undefined : parseInt(values.slotId || '0'), + isPermanent: values.isPermanent, + skuIds: values.skuIds.map(id => parseInt(id)), + validTill: values.validTill ? values.validTill.toISOString() : undefined, + } + + if (isEditing && snippet) { + await updateSnippet.mutateAsync({ + id: snippet.id, + updates: submitData, + }) + window.alert('Success: Vendor snippet updated successfully') + } else { + await createSnippet.mutateAsync(submitData) + window.alert('Success: Vendor snippet created successfully') + } + + onSuccess() + onClose() + } catch (error: any) { + window.alert(`Error: ${error.message || 'Failed to save vendor snippet'}`) + } + }, + }) + + // Generate unique snippet code if creating new (only on mount) + useEffect(() => { + if (!isEditing && !formik.values.snippetCode) { + const timestamp = Date.now() + const random = Math.random().toString(36).substring(2, 8) + formik.setFieldValue('snippetCode', `VS_${timestamp}_${random}`) + } + }, [isEditing]) // Removed formik.values.snippetCode from deps + + const slotOptions = slotsData?.slots.map(slot => ({ + label: new Date(slot.deliveryTime).toLocaleString(), + value: slot.id.toString(), + })) || [] + + return ( +
+
+
+

+ {isEditing ? 'Edit Vendor Snippet' : 'Create Vendor Snippet'} +

+ +
+
+ +
+
+ {/* Snippet Code */} +
+ + {formik.errors.snippetCode && formik.touched.snippetCode && ( +

{formik.errors.snippetCode}

+ )} +
+ + {/* Is Permanent Checkbox */} +
+ +

+ Check if this snippet is permanent and not tied to a specific delivery slot +

+
+ + {/* Slot Selection - Only show if not permanent */} + {!formik.values.isPermanent && ( +
+

Delivery Slot

+ formik.setFieldValue('slotId', String(value))} + className="w-full" + /> + {formik.errors.slotId && formik.touched.slotId && ( +

{formik.errors.slotId}

+ )} +
+ )} + + {/* Product Selection */} +
+ parseInt(id))} + onChange={(selectedProductIds) => formik.setFieldValue('skuIds', (selectedProductIds as number[]).map(id => id.toString()))} + multiple={true} + label="Select Products" + placeholder="Select products" + labelFormat={(product) => product.label} + /> + {formik.errors.skuIds && formik.touched.skuIds && ( +

{formik.errors.skuIds}

+ )} +
+ + {/* Valid Till Date */} +
+

Valid Till (Optional)

+ formik.setFieldValue('validTill', date)} + placeholder="Select expiry date" + showLabel={false} + /> +

+ Leave empty for no expiry +

+
+
+
+ + {/* Submit Button */} +
+ +
+
+ ) +} + +export default VendorSnippetForm diff --git a/apps/admin-web/src/components/context/staff-auth-context.tsx b/apps/admin-web/src/components/context/staff-auth-context.tsx new file mode 100644 index 0000000..75b8413 --- /dev/null +++ b/apps/admin-web/src/components/context/staff-auth-context.tsx @@ -0,0 +1,112 @@ +import React, { createContext, useContext, useEffect, useState, type ReactNode } from 'react' +import { useNavigate, useLocation } from '@tanstack/react-router' +import queryClient from '../../lib/query-client' +import { EventBus, FORCE_LOGOUT_EVENT } from '../../lib/event-bus' +import { trpc } from '../../lib/trpc-client' +import { saveJWT, getJWT, deleteJWT } from '../../hooks/useJWT' +import type { IdName } from '@packages/shared' + +type Staff = IdName + +interface StaffAuthContextType { + isLoggedIn: boolean + isLoading: boolean + staff: Staff | null + login: (name: string, password: string) => Promise + logout: () => void + isLoggingIn: boolean + loginError: string | null +} + +const StaffAuthContext = createContext(undefined) + +export const StaffAuthProvider = ({ children }: { children: ReactNode }) => { + const [isLoggedIn, setIsLoggedIn] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const [staff, setStaff] = useState(null) + const [loginError, setLoginError] = useState(null) + + const loginMutation = trpc.admin.staffUser.login.useMutation() + const navigate = useNavigate() + const location = useLocation() + + useEffect(() => { + const checkAuth = async () => { + const token = await getJWT() + if (token) { + setIsLoggedIn(true) + } else { + setIsLoggedIn(false) + if (!location.pathname.includes('login')) { + navigate({ to: '/login' }) + } + } + setIsLoading(false) + } + + checkAuth() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const login = async (name: string, password: string) => { + setLoginError(null) + loginMutation.mutate( + { name, password }, + { + onSuccess: async (data) => { + await saveJWT(data.token) + setStaff(data.staff) + setIsLoggedIn(true) + navigate({ to: '/dashboard' }) + }, + onError: (error: any) => { + setLoginError(error.message || 'Login failed') + }, + }, + ) + } + + const logout = async () => { + await deleteJWT() + setIsLoggedIn(false) + setStaff(null) + queryClient.clear() + navigate({ to: '/login' }) + } + + useEffect(() => { + const subscription = EventBus.addListener(FORCE_LOGOUT_EVENT, () => { + console.log('force logout event received') + logout() + }) + + return () => { + subscription.remove() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + + {children} + + ) +} + +export const useStaffAuth = () => { + const context = useContext(StaffAuthContext) + if (!context) { + throw new Error('useStaffAuth must be used within a StaffAuthProvider') + } + return context +} diff --git a/apps/admin-web/src/hooks/useJWT.ts b/apps/admin-web/src/hooks/useJWT.ts new file mode 100644 index 0000000..9b5ab24 --- /dev/null +++ b/apps/admin-web/src/hooks/useJWT.ts @@ -0,0 +1,15 @@ +import { StorageService } from 'web-components' + +export const JWT_KEY = 'jwt_token' + +export async function saveJWT(token: string) { + await StorageService.setItem(JWT_KEY, token) +} + +export async function getJWT() { + return await StorageService.getItem(JWT_KEY) +} + +export async function deleteJWT() { + await StorageService.removeItem(JWT_KEY) +} diff --git a/apps/admin-web/src/hooks/useUploadToObjectStorage.ts b/apps/admin-web/src/hooks/useUploadToObjectStorage.ts new file mode 100644 index 0000000..a420b7c --- /dev/null +++ b/apps/admin-web/src/hooks/useUploadToObjectStorage.ts @@ -0,0 +1,105 @@ +import { useState } from 'react' +import { trpc } from '../lib/trpc-client' +import type { ContextString, UploadInput, UploadBatchInput, UploadResult } from '@packages/shared' + +// Verbatim port of admin-ui useUploadToObjectStore (trpc import path only). +export function useUploadToObjectStorage() { + const [isUploading, setIsUploading] = useState(false) + const [error, setError] = useState(null) + const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null) + + const generateUploadUrls = trpc.common.generateUploadUrls.useMutation() + + const upload = async (input: UploadBatchInput): Promise => { + setIsUploading(true) + setError(null) + setProgress({ completed: 0, total: input.images.length }) + + try { + const { images, contextString } = input + + if (images.length === 0) { + return { keys: [], presignedUrls: [] } + } + + const mimeTypes = images.map((img) => img.mimeType) + const { uploadUrls } = await generateUploadUrls.mutateAsync({ + contextString, + mimeTypes, + }) + + if (uploadUrls.length !== images.length) { + throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`) + } + + const uploadPromises = images.map(async (image, index) => { + const presignedUrl = uploadUrls[index] + const { blob, mimeType } = image + + const response = await fetch(presignedUrl, { + method: 'PUT', + body: blob, + headers: { 'Content-Type': mimeType }, + }) + + if (!response.ok) { + throw new Error(`Upload ${index + 1} failed with status ${response.status}`) + } + + setProgress((prev) => (prev ? { ...prev, completed: prev.completed + 1 } : null)) + + return { + key: extractKeyFromPresignedUrl(presignedUrl), + presignedUrl, + } + }) + + const results = await Promise.all(uploadPromises) + + return { + keys: results.map((r) => r.key), + presignedUrls: results.map((r) => r.presignedUrl), + } + } catch (err) { + const uploadError = err instanceof Error ? err : new Error('Upload failed') + setError(uploadError) + throw uploadError + } finally { + setIsUploading(false) + setProgress(null) + } + } + + const uploadSingle = async ( + blob: Blob, + mimeType: string, + contextString: ContextString, + ): Promise<{ key: string; presignedUrl: string }> => { + const result = await upload({ + images: [{ blob, mimeType }], + contextString, + }) + return { + key: result.keys[0], + presignedUrl: result.presignedUrls[0], + } + } + + return { + upload, + uploadSingle, + isUploading, + error, + progress, + isPending: generateUploadUrls.isPending, + } +} + +function extractKeyFromPresignedUrl(url: string): string { + const u = new URL(url) + let rawKey = u.pathname.replace(/^\/+/, '') + rawKey = rawKey.split('/').slice(1).join('/') + return decodeURIComponent(rawKey) +} + +export type { UploadInput } diff --git a/apps/admin-web/src/lib/event-bus.ts b/apps/admin-web/src/lib/event-bus.ts new file mode 100644 index 0000000..775eae8 --- /dev/null +++ b/apps/admin-web/src/lib/event-bus.ts @@ -0,0 +1,33 @@ +// Web replacement for react-native's DeviceEventEmitter + shared event names. +// API-compatible subset: emit(event), addListener(event, cb) -> { remove() }. +export const REFRESH_EVENT = 'refresh_event' +export const FORCE_LOGOUT_EVENT = 'forceLogout' + +type Listener = (...args: any[]) => void + +const listeners = new Map>() + +export const EventBus = { + emit(event: string, ...args: any[]) { + listeners.get(event)?.forEach((cb) => { + try { + cb(...args) + } catch (e) { + console.error(`EventBus listener error for ${event}`, e) + } + }) + }, + addListener(event: string, cb: Listener) { + let set = listeners.get(event) + if (!set) { + set = new Set() + listeners.set(event, set) + } + set.add(cb) + return { + remove() { + listeners.get(event)?.delete(cb) + }, + } + }, +} diff --git a/apps/admin-web/src/lib/navigation-target.ts b/apps/admin-web/src/lib/navigation-target.ts new file mode 100644 index 0000000..508423b --- /dev/null +++ b/apps/admin-web/src/lib/navigation-target.ts @@ -0,0 +1,26 @@ +import { create } from 'zustand' + +// Verbatim port of common-ui useNavigationTarget (only dep: zustand). +interface NavigationTargetState { + navigationTarget: string | null + setNavigationTarget: (target: string | null) => void + getNavigationTarget: () => string | null +} + +const useNavigationTargetStore = create((set, get) => ({ + navigationTarget: null, + setNavigationTarget: (target: string | null) => { + set({ navigationTarget: target }) + }, + getNavigationTarget: () => { + const target = get().navigationTarget + if (target) { + set({ navigationTarget: null }) + } + return target + }, +})) + +export function useNavigationTarget() { + return useNavigationTargetStore() +} diff --git a/apps/admin-web/src/lib/query-client.ts b/apps/admin-web/src/lib/query-client.ts new file mode 100644 index 0000000..bade5e9 --- /dev/null +++ b/apps/admin-web/src/lib/query-client.ts @@ -0,0 +1,19 @@ +import { QueryClient } from '@tanstack/react-query' + +// Module-level singleton — same options as admin-ui (retry: 3). +let queryClient: QueryClient | undefined + +export function getQueryClient() { + if (!queryClient) { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 3, + }, + }, + }) + } + return queryClient +} + +export default getQueryClient() diff --git a/apps/admin-web/src/lib/refresh-context.tsx b/apps/admin-web/src/lib/refresh-context.tsx new file mode 100644 index 0000000..45b94d6 --- /dev/null +++ b/apps/admin-web/src/lib/refresh-context.tsx @@ -0,0 +1,62 @@ +import React, { createContext, useContext, useEffect, useRef } from 'react' +import { useLocation } from '@tanstack/react-router' +import { EventBus, REFRESH_EVENT } from './event-bus' +import { getQueryClient } from './query-client' + +// Web port of packages/ui refresh-context + useManualRefresh + +// useMarkDataFetchers + useFocusCallback. Navigation focus (expo) is +// approximated with route-pathname changes; manual refresh uses the bus. + +const RefreshContext = createContext<{ refreshAll: () => void } | null>(null) + +export const RefreshProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const refreshAll = () => { + getQueryClient().invalidateQueries() + EventBus.emit(REFRESH_EVENT) + } + return {children} +} + +export const useRefresh = () => { + const context = useContext(RefreshContext) + if (!context) throw new Error('useRefresh must be used within RefreshProvider') + return context +} + +export function useManualRefresh(callback: () => void) { + const ref = useRef(callback) + ref.current = callback + useEffect(() => { + const sub = EventBus.addListener(REFRESH_EVENT, () => ref.current()) + return () => sub.remove() + }, []) +} + +// Refetch on 2nd+ visit to the current route (mirrors useMarkDataFetchers: +// skip first focus, refetch on subsequent focuses). +export function useMarkDataFetchers(callback: () => void) { + const location = useLocation() + const ref = useRef(callback) + ref.current = callback + const visitsRef = useRef>({}) + useEffect(() => { + const key = location.pathname + location.searchStr + const count = visitsRef.current[key] ?? 0 + visitsRef.current[key] = count + 1 + if (count >= 1) { + ref.current() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.pathname, location.searchStr]) +} + +// Run callback when the window regains focus (web analogue of screen focus). +export default function useFocusCallback(callback: () => void) { + const ref = useRef(callback) + ref.current = callback + useEffect(() => { + const onFocus = () => ref.current() + window.addEventListener('focus', onFocus) + return () => window.removeEventListener('focus', onFocus) + }, []) +} diff --git a/apps/admin-web/src/lib/theme.ts b/apps/admin-web/src/lib/theme.ts new file mode 100644 index 0000000..372fe1f --- /dev/null +++ b/apps/admin-web/src/lib/theme.ts @@ -0,0 +1,60 @@ +// Exact copy of theme.colors from packages/ui (theme-colors.ts), which the +// web bundle cannot import (RN code). Keeps admin-web visuals identical. +export const colors = { + brand25: '#F5FAFF', + brand50: '#EFF8FF', + brand100: '#D1E9FF', + brand200: '#B2DDFF', + brand300: '#84CAFF', + brand400: '#53B1FD', + brand500: '#2E90FA', + brand600: '#1570EF', + brand700: '#175CD3', + brand800: '#1849A9', + brand900: '#194185', + blue1: '#2E90FA', + blue2: '#fff0f6', + pink1: '#2E90FA', + pink2: '#fff0f6', + blue3: '#ECF4FF', + red1: '#D84343', + green1: '#4CAF50', + green2: '#C8F4D3', + black1: '#000000', + black2: '#1A1C1E', + white1: '#FFFFFF', + gray1: '#fdfdfd', + gray2: '#f2f2f2', + gray3: '#F5F5F5', + gray4: '#6C7278', + gray5: '#ced4da', + yellow1: '#FFB74D', + yellow2: '#FFE3AD', + purple25: '#FAFAFF', + purple50: '#F4F3FF', + purple100: '#EBE9FE', + purple200: '#D9D6FE', + purple300: '#BDB4FE', + purple400: '#9B8AFB', + purple500: '#7A5AF8', + purple600: '#6938EF', + purple700: '#5925DC', + purple800: '#4A1FB8', + purple900: '#3E1C96', + gray25: '#FDFDFD', + gray50: '#FAFAFA', + gray100: '#F5F5F5', + gray200: '#E9EAEB', + gray300: '#D5D7DA', + gray400: '#A4A7AE', + gray500: '#717680', + gray600: '#535862', + gray700: '#414651', + gray800: '#252B37', + gray900: '#101828', + secondaryPink: '#fff', +} + +export const theme = { colors } + +export type ThemeColor = keyof typeof colors diff --git a/apps/admin-web/src/lib/trpc-client.ts b/apps/admin-web/src/lib/trpc-client.ts new file mode 100644 index 0000000..5ad6025 --- /dev/null +++ b/apps/admin-web/src/lib/trpc-client.ts @@ -0,0 +1,39 @@ +import { createTRPCReact } from '@trpc/react-query' +import { createTRPCClient, httpBatchLink } from '@trpc/client' +import type { AppRouter } from '@backend/trpc/router' +import { getJWT } from '../hooks/useJWT' +import { EventBus, FORCE_LOGOUT_EVENT } from './event-bus' + +// Same backend as admin-ui (BASE_API_URL from common-ui); overridable via env. +export const BASE_API_URL = + (import.meta.env?.VITE_API_URL as string | undefined) || 'https://devapi.freshyo.in' + +export const trpc = createTRPCReact() + +export function getTrpcClient() { + return createTRPCClient({ + links: [ + httpBatchLink({ + url: `${BASE_API_URL}/api/trpc`, + headers: async () => { + const token = await getJWT() + return token ? { Authorization: `Bearer ${token}` } : {} + }, + fetch: async (url, options) => { + const response = await fetch(url, options) + if (response.status === 401) { + const data = await response.clone().json().catch(() => ({})) + let code = '' + if (data[0]?.error || data?.error) { + code = data[0]?.error?.message || data?.error?.message + } + if (code === 'UNAUTHORIZED') { + EventBus.emit(FORCE_LOGOUT_EVENT) + } + } + return response + }, + }), + ], + }) +} diff --git a/apps/admin-web/src/routeTree.gen.ts b/apps/admin-web/src/routeTree.gen.ts new file mode 100644 index 0000000..57b59d0 --- /dev/null +++ b/apps/admin-web/src/routeTree.gen.ts @@ -0,0 +1,1047 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as LoginRouteImport } from './routes/login' +import { Route as DashboardRouteImport } from './routes/dashboard' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DashboardIndexRouteImport } from './routes/dashboard.index' +import { Route as DashboardVendorSnippetsRouteImport } from './routes/dashboard.vendor-snippets' +import { Route as DashboardUsersRouteImport } from './routes/dashboard.users' +import { Route as DashboardStoresRouteImport } from './routes/dashboard.stores' +import { Route as DashboardSlotsRouteImport } from './routes/dashboard.slots' +import { Route as DashboardRebalanceRouteImport } from './routes/dashboard.rebalance' +import { Route as DashboardProductsRouteImport } from './routes/dashboard.products' +import { Route as DashboardProductTagsRouteImport } from './routes/dashboard.product-tags' +import { Route as DashboardProductGroupingsRouteImport } from './routes/dashboard.product-groupings' +import { Route as DashboardPricesRouteImport } from './routes/dashboard.prices' +import { Route as DashboardOrdersRouteImport } from './routes/dashboard.orders' +import { Route as DashboardNotificationsRouteImport } from './routes/dashboard.notifications' +import { Route as DashboardCustomizeAppRouteImport } from './routes/dashboard.customize-app' +import { Route as DashboardCouponsRouteImport } from './routes/dashboard.coupons' +import { Route as DashboardComplaintsRouteImport } from './routes/dashboard.complaints' +import { Route as DashboardBannersRouteImport } from './routes/dashboard.banners' +import { Route as DashboardUsersIdRouteImport } from './routes/dashboard.users.$id' +import { Route as DashboardStoresNewRouteImport } from './routes/dashboard.stores.new' +import { Route as DashboardStoresEditRouteImport } from './routes/dashboard.stores.edit' +import { Route as DashboardSlotsNewRouteImport } from './routes/dashboard.slots.new' +import { Route as DashboardSlotsDetailRouteImport } from './routes/dashboard.slots.detail' +import { Route as DashboardProductsNewRouteImport } from './routes/dashboard.products.new' +import { Route as DashboardProductsEditRouteImport } from './routes/dashboard.products.edit' +import { Route as DashboardProductsIdRouteImport } from './routes/dashboard.products.$id' +import { Route as DashboardProductTagsOrderRouteImport } from './routes/dashboard.product-tags.order' +import { Route as DashboardProductTagsNewRouteImport } from './routes/dashboard.product-tags.new' +import { Route as DashboardProductTagsEditRouteImport } from './routes/dashboard.product-tags.edit' +import { Route as DashboardProductGroupingsNewRouteImport } from './routes/dashboard.product-groupings.new' +import { Route as DashboardOrdersSequenceRouteImport } from './routes/dashboard.orders.sequence' +import { Route as DashboardOrdersListRouteImport } from './routes/dashboard.orders.list' +import { Route as DashboardOrdersIdRouteImport } from './routes/dashboard.orders.$id' +import { Route as DashboardCustomizeAppPopularRouteImport } from './routes/dashboard.customize-app.popular' +import { Route as DashboardCustomizeAppOrderingRouteImport } from './routes/dashboard.customize-app.ordering' +import { Route as DashboardCouponsNewRouteImport } from './routes/dashboard.coupons.new' +import { Route as DashboardBannersNewRouteImport } from './routes/dashboard.banners.new' +import { Route as DashboardSlotsIdEditRouteImport } from './routes/dashboard.slots.$id.edit' +import { Route as DashboardProductGroupingsIdEditRouteImport } from './routes/dashboard.product-groupings.$id.edit' +import { Route as DashboardCouponsIdEditRouteImport } from './routes/dashboard.coupons.$id.edit' +import { Route as DashboardBannersIdEditRouteImport } from './routes/dashboard.banners.$id.edit' + +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) +const DashboardRoute = DashboardRouteImport.update({ + id: '/dashboard', + path: '/dashboard', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DashboardIndexRoute = DashboardIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardVendorSnippetsRoute = DashboardVendorSnippetsRouteImport.update({ + id: '/vendor-snippets', + path: '/vendor-snippets', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardUsersRoute = DashboardUsersRouteImport.update({ + id: '/users', + path: '/users', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardStoresRoute = DashboardStoresRouteImport.update({ + id: '/stores', + path: '/stores', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardSlotsRoute = DashboardSlotsRouteImport.update({ + id: '/slots', + path: '/slots', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardRebalanceRoute = DashboardRebalanceRouteImport.update({ + id: '/rebalance', + path: '/rebalance', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardProductsRoute = DashboardProductsRouteImport.update({ + id: '/products', + path: '/products', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardProductTagsRoute = DashboardProductTagsRouteImport.update({ + id: '/product-tags', + path: '/product-tags', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardProductGroupingsRoute = + DashboardProductGroupingsRouteImport.update({ + id: '/product-groupings', + path: '/product-groupings', + getParentRoute: () => DashboardRoute, + } as any) +const DashboardPricesRoute = DashboardPricesRouteImport.update({ + id: '/prices', + path: '/prices', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardOrdersRoute = DashboardOrdersRouteImport.update({ + id: '/orders', + path: '/orders', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardNotificationsRoute = DashboardNotificationsRouteImport.update({ + id: '/notifications', + path: '/notifications', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardCustomizeAppRoute = DashboardCustomizeAppRouteImport.update({ + id: '/customize-app', + path: '/customize-app', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardCouponsRoute = DashboardCouponsRouteImport.update({ + id: '/coupons', + path: '/coupons', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardComplaintsRoute = DashboardComplaintsRouteImport.update({ + id: '/complaints', + path: '/complaints', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardBannersRoute = DashboardBannersRouteImport.update({ + id: '/banners', + path: '/banners', + getParentRoute: () => DashboardRoute, +} as any) +const DashboardUsersIdRoute = DashboardUsersIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => DashboardUsersRoute, +} as any) +const DashboardStoresNewRoute = DashboardStoresNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => DashboardStoresRoute, +} as any) +const DashboardStoresEditRoute = DashboardStoresEditRouteImport.update({ + id: '/edit', + path: '/edit', + getParentRoute: () => DashboardStoresRoute, +} as any) +const DashboardSlotsNewRoute = DashboardSlotsNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => DashboardSlotsRoute, +} as any) +const DashboardSlotsDetailRoute = DashboardSlotsDetailRouteImport.update({ + id: '/detail', + path: '/detail', + getParentRoute: () => DashboardSlotsRoute, +} as any) +const DashboardProductsNewRoute = DashboardProductsNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => DashboardProductsRoute, +} as any) +const DashboardProductsEditRoute = DashboardProductsEditRouteImport.update({ + id: '/edit', + path: '/edit', + getParentRoute: () => DashboardProductsRoute, +} as any) +const DashboardProductsIdRoute = DashboardProductsIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => DashboardProductsRoute, +} as any) +const DashboardProductTagsOrderRoute = + DashboardProductTagsOrderRouteImport.update({ + id: '/order', + path: '/order', + getParentRoute: () => DashboardProductTagsRoute, + } as any) +const DashboardProductTagsNewRoute = DashboardProductTagsNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => DashboardProductTagsRoute, +} as any) +const DashboardProductTagsEditRoute = + DashboardProductTagsEditRouteImport.update({ + id: '/edit', + path: '/edit', + getParentRoute: () => DashboardProductTagsRoute, + } as any) +const DashboardProductGroupingsNewRoute = + DashboardProductGroupingsNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => DashboardProductGroupingsRoute, + } as any) +const DashboardOrdersSequenceRoute = DashboardOrdersSequenceRouteImport.update({ + id: '/sequence', + path: '/sequence', + getParentRoute: () => DashboardOrdersRoute, +} as any) +const DashboardOrdersListRoute = DashboardOrdersListRouteImport.update({ + id: '/list', + path: '/list', + getParentRoute: () => DashboardOrdersRoute, +} as any) +const DashboardOrdersIdRoute = DashboardOrdersIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => DashboardOrdersRoute, +} as any) +const DashboardCustomizeAppPopularRoute = + DashboardCustomizeAppPopularRouteImport.update({ + id: '/popular', + path: '/popular', + getParentRoute: () => DashboardCustomizeAppRoute, + } as any) +const DashboardCustomizeAppOrderingRoute = + DashboardCustomizeAppOrderingRouteImport.update({ + id: '/ordering', + path: '/ordering', + getParentRoute: () => DashboardCustomizeAppRoute, + } as any) +const DashboardCouponsNewRoute = DashboardCouponsNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => DashboardCouponsRoute, +} as any) +const DashboardBannersNewRoute = DashboardBannersNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => DashboardBannersRoute, +} as any) +const DashboardSlotsIdEditRoute = DashboardSlotsIdEditRouteImport.update({ + id: '/$id/edit', + path: '/$id/edit', + getParentRoute: () => DashboardSlotsRoute, +} as any) +const DashboardProductGroupingsIdEditRoute = + DashboardProductGroupingsIdEditRouteImport.update({ + id: '/$id/edit', + path: '/$id/edit', + getParentRoute: () => DashboardProductGroupingsRoute, + } as any) +const DashboardCouponsIdEditRoute = DashboardCouponsIdEditRouteImport.update({ + id: '/$id/edit', + path: '/$id/edit', + getParentRoute: () => DashboardCouponsRoute, +} as any) +const DashboardBannersIdEditRoute = DashboardBannersIdEditRouteImport.update({ + id: '/$id/edit', + path: '/$id/edit', + getParentRoute: () => DashboardBannersRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteWithChildren + '/login': typeof LoginRoute + '/dashboard/banners': typeof DashboardBannersRouteWithChildren + '/dashboard/complaints': typeof DashboardComplaintsRoute + '/dashboard/coupons': typeof DashboardCouponsRouteWithChildren + '/dashboard/customize-app': typeof DashboardCustomizeAppRouteWithChildren + '/dashboard/notifications': typeof DashboardNotificationsRoute + '/dashboard/orders': typeof DashboardOrdersRouteWithChildren + '/dashboard/prices': typeof DashboardPricesRoute + '/dashboard/product-groupings': typeof DashboardProductGroupingsRouteWithChildren + '/dashboard/product-tags': typeof DashboardProductTagsRouteWithChildren + '/dashboard/products': typeof DashboardProductsRouteWithChildren + '/dashboard/rebalance': typeof DashboardRebalanceRoute + '/dashboard/slots': typeof DashboardSlotsRouteWithChildren + '/dashboard/stores': typeof DashboardStoresRouteWithChildren + '/dashboard/users': typeof DashboardUsersRouteWithChildren + '/dashboard/vendor-snippets': typeof DashboardVendorSnippetsRoute + '/dashboard/': typeof DashboardIndexRoute + '/dashboard/banners/new': typeof DashboardBannersNewRoute + '/dashboard/coupons/new': typeof DashboardCouponsNewRoute + '/dashboard/customize-app/ordering': typeof DashboardCustomizeAppOrderingRoute + '/dashboard/customize-app/popular': typeof DashboardCustomizeAppPopularRoute + '/dashboard/orders/$id': typeof DashboardOrdersIdRoute + '/dashboard/orders/list': typeof DashboardOrdersListRoute + '/dashboard/orders/sequence': typeof DashboardOrdersSequenceRoute + '/dashboard/product-groupings/new': typeof DashboardProductGroupingsNewRoute + '/dashboard/product-tags/edit': typeof DashboardProductTagsEditRoute + '/dashboard/product-tags/new': typeof DashboardProductTagsNewRoute + '/dashboard/product-tags/order': typeof DashboardProductTagsOrderRoute + '/dashboard/products/$id': typeof DashboardProductsIdRoute + '/dashboard/products/edit': typeof DashboardProductsEditRoute + '/dashboard/products/new': typeof DashboardProductsNewRoute + '/dashboard/slots/detail': typeof DashboardSlotsDetailRoute + '/dashboard/slots/new': typeof DashboardSlotsNewRoute + '/dashboard/stores/edit': typeof DashboardStoresEditRoute + '/dashboard/stores/new': typeof DashboardStoresNewRoute + '/dashboard/users/$id': typeof DashboardUsersIdRoute + '/dashboard/banners/$id/edit': typeof DashboardBannersIdEditRoute + '/dashboard/coupons/$id/edit': typeof DashboardCouponsIdEditRoute + '/dashboard/product-groupings/$id/edit': typeof DashboardProductGroupingsIdEditRoute + '/dashboard/slots/$id/edit': typeof DashboardSlotsIdEditRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/dashboard/banners': typeof DashboardBannersRouteWithChildren + '/dashboard/complaints': typeof DashboardComplaintsRoute + '/dashboard/coupons': typeof DashboardCouponsRouteWithChildren + '/dashboard/customize-app': typeof DashboardCustomizeAppRouteWithChildren + '/dashboard/notifications': typeof DashboardNotificationsRoute + '/dashboard/orders': typeof DashboardOrdersRouteWithChildren + '/dashboard/prices': typeof DashboardPricesRoute + '/dashboard/product-groupings': typeof DashboardProductGroupingsRouteWithChildren + '/dashboard/product-tags': typeof DashboardProductTagsRouteWithChildren + '/dashboard/products': typeof DashboardProductsRouteWithChildren + '/dashboard/rebalance': typeof DashboardRebalanceRoute + '/dashboard/slots': typeof DashboardSlotsRouteWithChildren + '/dashboard/stores': typeof DashboardStoresRouteWithChildren + '/dashboard/users': typeof DashboardUsersRouteWithChildren + '/dashboard/vendor-snippets': typeof DashboardVendorSnippetsRoute + '/dashboard': typeof DashboardIndexRoute + '/dashboard/banners/new': typeof DashboardBannersNewRoute + '/dashboard/coupons/new': typeof DashboardCouponsNewRoute + '/dashboard/customize-app/ordering': typeof DashboardCustomizeAppOrderingRoute + '/dashboard/customize-app/popular': typeof DashboardCustomizeAppPopularRoute + '/dashboard/orders/$id': typeof DashboardOrdersIdRoute + '/dashboard/orders/list': typeof DashboardOrdersListRoute + '/dashboard/orders/sequence': typeof DashboardOrdersSequenceRoute + '/dashboard/product-groupings/new': typeof DashboardProductGroupingsNewRoute + '/dashboard/product-tags/edit': typeof DashboardProductTagsEditRoute + '/dashboard/product-tags/new': typeof DashboardProductTagsNewRoute + '/dashboard/product-tags/order': typeof DashboardProductTagsOrderRoute + '/dashboard/products/$id': typeof DashboardProductsIdRoute + '/dashboard/products/edit': typeof DashboardProductsEditRoute + '/dashboard/products/new': typeof DashboardProductsNewRoute + '/dashboard/slots/detail': typeof DashboardSlotsDetailRoute + '/dashboard/slots/new': typeof DashboardSlotsNewRoute + '/dashboard/stores/edit': typeof DashboardStoresEditRoute + '/dashboard/stores/new': typeof DashboardStoresNewRoute + '/dashboard/users/$id': typeof DashboardUsersIdRoute + '/dashboard/banners/$id/edit': typeof DashboardBannersIdEditRoute + '/dashboard/coupons/$id/edit': typeof DashboardCouponsIdEditRoute + '/dashboard/product-groupings/$id/edit': typeof DashboardProductGroupingsIdEditRoute + '/dashboard/slots/$id/edit': typeof DashboardSlotsIdEditRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteWithChildren + '/login': typeof LoginRoute + '/dashboard/banners': typeof DashboardBannersRouteWithChildren + '/dashboard/complaints': typeof DashboardComplaintsRoute + '/dashboard/coupons': typeof DashboardCouponsRouteWithChildren + '/dashboard/customize-app': typeof DashboardCustomizeAppRouteWithChildren + '/dashboard/notifications': typeof DashboardNotificationsRoute + '/dashboard/orders': typeof DashboardOrdersRouteWithChildren + '/dashboard/prices': typeof DashboardPricesRoute + '/dashboard/product-groupings': typeof DashboardProductGroupingsRouteWithChildren + '/dashboard/product-tags': typeof DashboardProductTagsRouteWithChildren + '/dashboard/products': typeof DashboardProductsRouteWithChildren + '/dashboard/rebalance': typeof DashboardRebalanceRoute + '/dashboard/slots': typeof DashboardSlotsRouteWithChildren + '/dashboard/stores': typeof DashboardStoresRouteWithChildren + '/dashboard/users': typeof DashboardUsersRouteWithChildren + '/dashboard/vendor-snippets': typeof DashboardVendorSnippetsRoute + '/dashboard/': typeof DashboardIndexRoute + '/dashboard/banners/new': typeof DashboardBannersNewRoute + '/dashboard/coupons/new': typeof DashboardCouponsNewRoute + '/dashboard/customize-app/ordering': typeof DashboardCustomizeAppOrderingRoute + '/dashboard/customize-app/popular': typeof DashboardCustomizeAppPopularRoute + '/dashboard/orders/$id': typeof DashboardOrdersIdRoute + '/dashboard/orders/list': typeof DashboardOrdersListRoute + '/dashboard/orders/sequence': typeof DashboardOrdersSequenceRoute + '/dashboard/product-groupings/new': typeof DashboardProductGroupingsNewRoute + '/dashboard/product-tags/edit': typeof DashboardProductTagsEditRoute + '/dashboard/product-tags/new': typeof DashboardProductTagsNewRoute + '/dashboard/product-tags/order': typeof DashboardProductTagsOrderRoute + '/dashboard/products/$id': typeof DashboardProductsIdRoute + '/dashboard/products/edit': typeof DashboardProductsEditRoute + '/dashboard/products/new': typeof DashboardProductsNewRoute + '/dashboard/slots/detail': typeof DashboardSlotsDetailRoute + '/dashboard/slots/new': typeof DashboardSlotsNewRoute + '/dashboard/stores/edit': typeof DashboardStoresEditRoute + '/dashboard/stores/new': typeof DashboardStoresNewRoute + '/dashboard/users/$id': typeof DashboardUsersIdRoute + '/dashboard/banners/$id/edit': typeof DashboardBannersIdEditRoute + '/dashboard/coupons/$id/edit': typeof DashboardCouponsIdEditRoute + '/dashboard/product-groupings/$id/edit': typeof DashboardProductGroupingsIdEditRoute + '/dashboard/slots/$id/edit': typeof DashboardSlotsIdEditRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/dashboard' + | '/login' + | '/dashboard/banners' + | '/dashboard/complaints' + | '/dashboard/coupons' + | '/dashboard/customize-app' + | '/dashboard/notifications' + | '/dashboard/orders' + | '/dashboard/prices' + | '/dashboard/product-groupings' + | '/dashboard/product-tags' + | '/dashboard/products' + | '/dashboard/rebalance' + | '/dashboard/slots' + | '/dashboard/stores' + | '/dashboard/users' + | '/dashboard/vendor-snippets' + | '/dashboard/' + | '/dashboard/banners/new' + | '/dashboard/coupons/new' + | '/dashboard/customize-app/ordering' + | '/dashboard/customize-app/popular' + | '/dashboard/orders/$id' + | '/dashboard/orders/list' + | '/dashboard/orders/sequence' + | '/dashboard/product-groupings/new' + | '/dashboard/product-tags/edit' + | '/dashboard/product-tags/new' + | '/dashboard/product-tags/order' + | '/dashboard/products/$id' + | '/dashboard/products/edit' + | '/dashboard/products/new' + | '/dashboard/slots/detail' + | '/dashboard/slots/new' + | '/dashboard/stores/edit' + | '/dashboard/stores/new' + | '/dashboard/users/$id' + | '/dashboard/banners/$id/edit' + | '/dashboard/coupons/$id/edit' + | '/dashboard/product-groupings/$id/edit' + | '/dashboard/slots/$id/edit' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/login' + | '/dashboard/banners' + | '/dashboard/complaints' + | '/dashboard/coupons' + | '/dashboard/customize-app' + | '/dashboard/notifications' + | '/dashboard/orders' + | '/dashboard/prices' + | '/dashboard/product-groupings' + | '/dashboard/product-tags' + | '/dashboard/products' + | '/dashboard/rebalance' + | '/dashboard/slots' + | '/dashboard/stores' + | '/dashboard/users' + | '/dashboard/vendor-snippets' + | '/dashboard' + | '/dashboard/banners/new' + | '/dashboard/coupons/new' + | '/dashboard/customize-app/ordering' + | '/dashboard/customize-app/popular' + | '/dashboard/orders/$id' + | '/dashboard/orders/list' + | '/dashboard/orders/sequence' + | '/dashboard/product-groupings/new' + | '/dashboard/product-tags/edit' + | '/dashboard/product-tags/new' + | '/dashboard/product-tags/order' + | '/dashboard/products/$id' + | '/dashboard/products/edit' + | '/dashboard/products/new' + | '/dashboard/slots/detail' + | '/dashboard/slots/new' + | '/dashboard/stores/edit' + | '/dashboard/stores/new' + | '/dashboard/users/$id' + | '/dashboard/banners/$id/edit' + | '/dashboard/coupons/$id/edit' + | '/dashboard/product-groupings/$id/edit' + | '/dashboard/slots/$id/edit' + id: + | '__root__' + | '/' + | '/dashboard' + | '/login' + | '/dashboard/banners' + | '/dashboard/complaints' + | '/dashboard/coupons' + | '/dashboard/customize-app' + | '/dashboard/notifications' + | '/dashboard/orders' + | '/dashboard/prices' + | '/dashboard/product-groupings' + | '/dashboard/product-tags' + | '/dashboard/products' + | '/dashboard/rebalance' + | '/dashboard/slots' + | '/dashboard/stores' + | '/dashboard/users' + | '/dashboard/vendor-snippets' + | '/dashboard/' + | '/dashboard/banners/new' + | '/dashboard/coupons/new' + | '/dashboard/customize-app/ordering' + | '/dashboard/customize-app/popular' + | '/dashboard/orders/$id' + | '/dashboard/orders/list' + | '/dashboard/orders/sequence' + | '/dashboard/product-groupings/new' + | '/dashboard/product-tags/edit' + | '/dashboard/product-tags/new' + | '/dashboard/product-tags/order' + | '/dashboard/products/$id' + | '/dashboard/products/edit' + | '/dashboard/products/new' + | '/dashboard/slots/detail' + | '/dashboard/slots/new' + | '/dashboard/stores/edit' + | '/dashboard/stores/new' + | '/dashboard/users/$id' + | '/dashboard/banners/$id/edit' + | '/dashboard/coupons/$id/edit' + | '/dashboard/product-groupings/$id/edit' + | '/dashboard/slots/$id/edit' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DashboardRoute: typeof DashboardRouteWithChildren + LoginRoute: typeof LoginRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard': { + id: '/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof DashboardRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/dashboard/': { + id: '/dashboard/' + path: '/' + fullPath: '/dashboard/' + preLoaderRoute: typeof DashboardIndexRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/vendor-snippets': { + id: '/dashboard/vendor-snippets' + path: '/vendor-snippets' + fullPath: '/dashboard/vendor-snippets' + preLoaderRoute: typeof DashboardVendorSnippetsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/users': { + id: '/dashboard/users' + path: '/users' + fullPath: '/dashboard/users' + preLoaderRoute: typeof DashboardUsersRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/stores': { + id: '/dashboard/stores' + path: '/stores' + fullPath: '/dashboard/stores' + preLoaderRoute: typeof DashboardStoresRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/slots': { + id: '/dashboard/slots' + path: '/slots' + fullPath: '/dashboard/slots' + preLoaderRoute: typeof DashboardSlotsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/rebalance': { + id: '/dashboard/rebalance' + path: '/rebalance' + fullPath: '/dashboard/rebalance' + preLoaderRoute: typeof DashboardRebalanceRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/products': { + id: '/dashboard/products' + path: '/products' + fullPath: '/dashboard/products' + preLoaderRoute: typeof DashboardProductsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/product-tags': { + id: '/dashboard/product-tags' + path: '/product-tags' + fullPath: '/dashboard/product-tags' + preLoaderRoute: typeof DashboardProductTagsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/product-groupings': { + id: '/dashboard/product-groupings' + path: '/product-groupings' + fullPath: '/dashboard/product-groupings' + preLoaderRoute: typeof DashboardProductGroupingsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/prices': { + id: '/dashboard/prices' + path: '/prices' + fullPath: '/dashboard/prices' + preLoaderRoute: typeof DashboardPricesRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/orders': { + id: '/dashboard/orders' + path: '/orders' + fullPath: '/dashboard/orders' + preLoaderRoute: typeof DashboardOrdersRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/notifications': { + id: '/dashboard/notifications' + path: '/notifications' + fullPath: '/dashboard/notifications' + preLoaderRoute: typeof DashboardNotificationsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/customize-app': { + id: '/dashboard/customize-app' + path: '/customize-app' + fullPath: '/dashboard/customize-app' + preLoaderRoute: typeof DashboardCustomizeAppRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/coupons': { + id: '/dashboard/coupons' + path: '/coupons' + fullPath: '/dashboard/coupons' + preLoaderRoute: typeof DashboardCouponsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/complaints': { + id: '/dashboard/complaints' + path: '/complaints' + fullPath: '/dashboard/complaints' + preLoaderRoute: typeof DashboardComplaintsRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/banners': { + id: '/dashboard/banners' + path: '/banners' + fullPath: '/dashboard/banners' + preLoaderRoute: typeof DashboardBannersRouteImport + parentRoute: typeof DashboardRoute + } + '/dashboard/users/$id': { + id: '/dashboard/users/$id' + path: '/$id' + fullPath: '/dashboard/users/$id' + preLoaderRoute: typeof DashboardUsersIdRouteImport + parentRoute: typeof DashboardUsersRoute + } + '/dashboard/stores/new': { + id: '/dashboard/stores/new' + path: '/new' + fullPath: '/dashboard/stores/new' + preLoaderRoute: typeof DashboardStoresNewRouteImport + parentRoute: typeof DashboardStoresRoute + } + '/dashboard/stores/edit': { + id: '/dashboard/stores/edit' + path: '/edit' + fullPath: '/dashboard/stores/edit' + preLoaderRoute: typeof DashboardStoresEditRouteImport + parentRoute: typeof DashboardStoresRoute + } + '/dashboard/slots/new': { + id: '/dashboard/slots/new' + path: '/new' + fullPath: '/dashboard/slots/new' + preLoaderRoute: typeof DashboardSlotsNewRouteImport + parentRoute: typeof DashboardSlotsRoute + } + '/dashboard/slots/detail': { + id: '/dashboard/slots/detail' + path: '/detail' + fullPath: '/dashboard/slots/detail' + preLoaderRoute: typeof DashboardSlotsDetailRouteImport + parentRoute: typeof DashboardSlotsRoute + } + '/dashboard/products/new': { + id: '/dashboard/products/new' + path: '/new' + fullPath: '/dashboard/products/new' + preLoaderRoute: typeof DashboardProductsNewRouteImport + parentRoute: typeof DashboardProductsRoute + } + '/dashboard/products/edit': { + id: '/dashboard/products/edit' + path: '/edit' + fullPath: '/dashboard/products/edit' + preLoaderRoute: typeof DashboardProductsEditRouteImport + parentRoute: typeof DashboardProductsRoute + } + '/dashboard/products/$id': { + id: '/dashboard/products/$id' + path: '/$id' + fullPath: '/dashboard/products/$id' + preLoaderRoute: typeof DashboardProductsIdRouteImport + parentRoute: typeof DashboardProductsRoute + } + '/dashboard/product-tags/order': { + id: '/dashboard/product-tags/order' + path: '/order' + fullPath: '/dashboard/product-tags/order' + preLoaderRoute: typeof DashboardProductTagsOrderRouteImport + parentRoute: typeof DashboardProductTagsRoute + } + '/dashboard/product-tags/new': { + id: '/dashboard/product-tags/new' + path: '/new' + fullPath: '/dashboard/product-tags/new' + preLoaderRoute: typeof DashboardProductTagsNewRouteImport + parentRoute: typeof DashboardProductTagsRoute + } + '/dashboard/product-tags/edit': { + id: '/dashboard/product-tags/edit' + path: '/edit' + fullPath: '/dashboard/product-tags/edit' + preLoaderRoute: typeof DashboardProductTagsEditRouteImport + parentRoute: typeof DashboardProductTagsRoute + } + '/dashboard/product-groupings/new': { + id: '/dashboard/product-groupings/new' + path: '/new' + fullPath: '/dashboard/product-groupings/new' + preLoaderRoute: typeof DashboardProductGroupingsNewRouteImport + parentRoute: typeof DashboardProductGroupingsRoute + } + '/dashboard/orders/sequence': { + id: '/dashboard/orders/sequence' + path: '/sequence' + fullPath: '/dashboard/orders/sequence' + preLoaderRoute: typeof DashboardOrdersSequenceRouteImport + parentRoute: typeof DashboardOrdersRoute + } + '/dashboard/orders/list': { + id: '/dashboard/orders/list' + path: '/list' + fullPath: '/dashboard/orders/list' + preLoaderRoute: typeof DashboardOrdersListRouteImport + parentRoute: typeof DashboardOrdersRoute + } + '/dashboard/orders/$id': { + id: '/dashboard/orders/$id' + path: '/$id' + fullPath: '/dashboard/orders/$id' + preLoaderRoute: typeof DashboardOrdersIdRouteImport + parentRoute: typeof DashboardOrdersRoute + } + '/dashboard/customize-app/popular': { + id: '/dashboard/customize-app/popular' + path: '/popular' + fullPath: '/dashboard/customize-app/popular' + preLoaderRoute: typeof DashboardCustomizeAppPopularRouteImport + parentRoute: typeof DashboardCustomizeAppRoute + } + '/dashboard/customize-app/ordering': { + id: '/dashboard/customize-app/ordering' + path: '/ordering' + fullPath: '/dashboard/customize-app/ordering' + preLoaderRoute: typeof DashboardCustomizeAppOrderingRouteImport + parentRoute: typeof DashboardCustomizeAppRoute + } + '/dashboard/coupons/new': { + id: '/dashboard/coupons/new' + path: '/new' + fullPath: '/dashboard/coupons/new' + preLoaderRoute: typeof DashboardCouponsNewRouteImport + parentRoute: typeof DashboardCouponsRoute + } + '/dashboard/banners/new': { + id: '/dashboard/banners/new' + path: '/new' + fullPath: '/dashboard/banners/new' + preLoaderRoute: typeof DashboardBannersNewRouteImport + parentRoute: typeof DashboardBannersRoute + } + '/dashboard/slots/$id/edit': { + id: '/dashboard/slots/$id/edit' + path: '/$id/edit' + fullPath: '/dashboard/slots/$id/edit' + preLoaderRoute: typeof DashboardSlotsIdEditRouteImport + parentRoute: typeof DashboardSlotsRoute + } + '/dashboard/product-groupings/$id/edit': { + id: '/dashboard/product-groupings/$id/edit' + path: '/$id/edit' + fullPath: '/dashboard/product-groupings/$id/edit' + preLoaderRoute: typeof DashboardProductGroupingsIdEditRouteImport + parentRoute: typeof DashboardProductGroupingsRoute + } + '/dashboard/coupons/$id/edit': { + id: '/dashboard/coupons/$id/edit' + path: '/$id/edit' + fullPath: '/dashboard/coupons/$id/edit' + preLoaderRoute: typeof DashboardCouponsIdEditRouteImport + parentRoute: typeof DashboardCouponsRoute + } + '/dashboard/banners/$id/edit': { + id: '/dashboard/banners/$id/edit' + path: '/$id/edit' + fullPath: '/dashboard/banners/$id/edit' + preLoaderRoute: typeof DashboardBannersIdEditRouteImport + parentRoute: typeof DashboardBannersRoute + } + } +} + +interface DashboardBannersRouteChildren { + DashboardBannersNewRoute: typeof DashboardBannersNewRoute + DashboardBannersIdEditRoute: typeof DashboardBannersIdEditRoute +} + +const DashboardBannersRouteChildren: DashboardBannersRouteChildren = { + DashboardBannersNewRoute: DashboardBannersNewRoute, + DashboardBannersIdEditRoute: DashboardBannersIdEditRoute, +} + +const DashboardBannersRouteWithChildren = + DashboardBannersRoute._addFileChildren(DashboardBannersRouteChildren) + +interface DashboardCouponsRouteChildren { + DashboardCouponsNewRoute: typeof DashboardCouponsNewRoute + DashboardCouponsIdEditRoute: typeof DashboardCouponsIdEditRoute +} + +const DashboardCouponsRouteChildren: DashboardCouponsRouteChildren = { + DashboardCouponsNewRoute: DashboardCouponsNewRoute, + DashboardCouponsIdEditRoute: DashboardCouponsIdEditRoute, +} + +const DashboardCouponsRouteWithChildren = + DashboardCouponsRoute._addFileChildren(DashboardCouponsRouteChildren) + +interface DashboardCustomizeAppRouteChildren { + DashboardCustomizeAppOrderingRoute: typeof DashboardCustomizeAppOrderingRoute + DashboardCustomizeAppPopularRoute: typeof DashboardCustomizeAppPopularRoute +} + +const DashboardCustomizeAppRouteChildren: DashboardCustomizeAppRouteChildren = { + DashboardCustomizeAppOrderingRoute: DashboardCustomizeAppOrderingRoute, + DashboardCustomizeAppPopularRoute: DashboardCustomizeAppPopularRoute, +} + +const DashboardCustomizeAppRouteWithChildren = + DashboardCustomizeAppRoute._addFileChildren( + DashboardCustomizeAppRouteChildren, + ) + +interface DashboardOrdersRouteChildren { + DashboardOrdersIdRoute: typeof DashboardOrdersIdRoute + DashboardOrdersListRoute: typeof DashboardOrdersListRoute + DashboardOrdersSequenceRoute: typeof DashboardOrdersSequenceRoute +} + +const DashboardOrdersRouteChildren: DashboardOrdersRouteChildren = { + DashboardOrdersIdRoute: DashboardOrdersIdRoute, + DashboardOrdersListRoute: DashboardOrdersListRoute, + DashboardOrdersSequenceRoute: DashboardOrdersSequenceRoute, +} + +const DashboardOrdersRouteWithChildren = DashboardOrdersRoute._addFileChildren( + DashboardOrdersRouteChildren, +) + +interface DashboardProductGroupingsRouteChildren { + DashboardProductGroupingsNewRoute: typeof DashboardProductGroupingsNewRoute + DashboardProductGroupingsIdEditRoute: typeof DashboardProductGroupingsIdEditRoute +} + +const DashboardProductGroupingsRouteChildren: DashboardProductGroupingsRouteChildren = + { + DashboardProductGroupingsNewRoute: DashboardProductGroupingsNewRoute, + DashboardProductGroupingsIdEditRoute: DashboardProductGroupingsIdEditRoute, + } + +const DashboardProductGroupingsRouteWithChildren = + DashboardProductGroupingsRoute._addFileChildren( + DashboardProductGroupingsRouteChildren, + ) + +interface DashboardProductTagsRouteChildren { + DashboardProductTagsEditRoute: typeof DashboardProductTagsEditRoute + DashboardProductTagsNewRoute: typeof DashboardProductTagsNewRoute + DashboardProductTagsOrderRoute: typeof DashboardProductTagsOrderRoute +} + +const DashboardProductTagsRouteChildren: DashboardProductTagsRouteChildren = { + DashboardProductTagsEditRoute: DashboardProductTagsEditRoute, + DashboardProductTagsNewRoute: DashboardProductTagsNewRoute, + DashboardProductTagsOrderRoute: DashboardProductTagsOrderRoute, +} + +const DashboardProductTagsRouteWithChildren = + DashboardProductTagsRoute._addFileChildren(DashboardProductTagsRouteChildren) + +interface DashboardProductsRouteChildren { + DashboardProductsIdRoute: typeof DashboardProductsIdRoute + DashboardProductsEditRoute: typeof DashboardProductsEditRoute + DashboardProductsNewRoute: typeof DashboardProductsNewRoute +} + +const DashboardProductsRouteChildren: DashboardProductsRouteChildren = { + DashboardProductsIdRoute: DashboardProductsIdRoute, + DashboardProductsEditRoute: DashboardProductsEditRoute, + DashboardProductsNewRoute: DashboardProductsNewRoute, +} + +const DashboardProductsRouteWithChildren = + DashboardProductsRoute._addFileChildren(DashboardProductsRouteChildren) + +interface DashboardSlotsRouteChildren { + DashboardSlotsDetailRoute: typeof DashboardSlotsDetailRoute + DashboardSlotsNewRoute: typeof DashboardSlotsNewRoute + DashboardSlotsIdEditRoute: typeof DashboardSlotsIdEditRoute +} + +const DashboardSlotsRouteChildren: DashboardSlotsRouteChildren = { + DashboardSlotsDetailRoute: DashboardSlotsDetailRoute, + DashboardSlotsNewRoute: DashboardSlotsNewRoute, + DashboardSlotsIdEditRoute: DashboardSlotsIdEditRoute, +} + +const DashboardSlotsRouteWithChildren = DashboardSlotsRoute._addFileChildren( + DashboardSlotsRouteChildren, +) + +interface DashboardStoresRouteChildren { + DashboardStoresEditRoute: typeof DashboardStoresEditRoute + DashboardStoresNewRoute: typeof DashboardStoresNewRoute +} + +const DashboardStoresRouteChildren: DashboardStoresRouteChildren = { + DashboardStoresEditRoute: DashboardStoresEditRoute, + DashboardStoresNewRoute: DashboardStoresNewRoute, +} + +const DashboardStoresRouteWithChildren = DashboardStoresRoute._addFileChildren( + DashboardStoresRouteChildren, +) + +interface DashboardUsersRouteChildren { + DashboardUsersIdRoute: typeof DashboardUsersIdRoute +} + +const DashboardUsersRouteChildren: DashboardUsersRouteChildren = { + DashboardUsersIdRoute: DashboardUsersIdRoute, +} + +const DashboardUsersRouteWithChildren = DashboardUsersRoute._addFileChildren( + DashboardUsersRouteChildren, +) + +interface DashboardRouteChildren { + DashboardBannersRoute: typeof DashboardBannersRouteWithChildren + DashboardComplaintsRoute: typeof DashboardComplaintsRoute + DashboardCouponsRoute: typeof DashboardCouponsRouteWithChildren + DashboardCustomizeAppRoute: typeof DashboardCustomizeAppRouteWithChildren + DashboardNotificationsRoute: typeof DashboardNotificationsRoute + DashboardOrdersRoute: typeof DashboardOrdersRouteWithChildren + DashboardPricesRoute: typeof DashboardPricesRoute + DashboardProductGroupingsRoute: typeof DashboardProductGroupingsRouteWithChildren + DashboardProductTagsRoute: typeof DashboardProductTagsRouteWithChildren + DashboardProductsRoute: typeof DashboardProductsRouteWithChildren + DashboardRebalanceRoute: typeof DashboardRebalanceRoute + DashboardSlotsRoute: typeof DashboardSlotsRouteWithChildren + DashboardStoresRoute: typeof DashboardStoresRouteWithChildren + DashboardUsersRoute: typeof DashboardUsersRouteWithChildren + DashboardVendorSnippetsRoute: typeof DashboardVendorSnippetsRoute + DashboardIndexRoute: typeof DashboardIndexRoute +} + +const DashboardRouteChildren: DashboardRouteChildren = { + DashboardBannersRoute: DashboardBannersRouteWithChildren, + DashboardComplaintsRoute: DashboardComplaintsRoute, + DashboardCouponsRoute: DashboardCouponsRouteWithChildren, + DashboardCustomizeAppRoute: DashboardCustomizeAppRouteWithChildren, + DashboardNotificationsRoute: DashboardNotificationsRoute, + DashboardOrdersRoute: DashboardOrdersRouteWithChildren, + DashboardPricesRoute: DashboardPricesRoute, + DashboardProductGroupingsRoute: DashboardProductGroupingsRouteWithChildren, + DashboardProductTagsRoute: DashboardProductTagsRouteWithChildren, + DashboardProductsRoute: DashboardProductsRouteWithChildren, + DashboardRebalanceRoute: DashboardRebalanceRoute, + DashboardSlotsRoute: DashboardSlotsRouteWithChildren, + DashboardStoresRoute: DashboardStoresRouteWithChildren, + DashboardUsersRoute: DashboardUsersRouteWithChildren, + DashboardVendorSnippetsRoute: DashboardVendorSnippetsRoute, + DashboardIndexRoute: DashboardIndexRoute, +} + +const DashboardRouteWithChildren = DashboardRoute._addFileChildren( + DashboardRouteChildren, +) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DashboardRoute: DashboardRouteWithChildren, + LoginRoute: LoginRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/apps/admin-web/src/router.tsx b/apps/admin-web/src/router.tsx new file mode 100644 index 0000000..e7b1c4d --- /dev/null +++ b/apps/admin-web/src/router.tsx @@ -0,0 +1,19 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultPreloadStaleTime: 0, + }) + + return router +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/apps/admin-web/src/routes/__root.tsx b/apps/admin-web/src/routes/__root.tsx new file mode 100644 index 0000000..0598760 --- /dev/null +++ b/apps/admin-web/src/routes/__root.tsx @@ -0,0 +1,64 @@ +import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' +import { TanStackDevtools } from '@tanstack/react-devtools' +import { QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { Toaster } from 'sonner' +import { trpc, getTrpcClient } from '../lib/trpc-client' +import queryClient from '../lib/query-client' +import { StaffAuthProvider } from '../components/context/staff-auth-context' +import { RefreshProvider } from '../lib/refresh-context' +import appCss from '../styles.css?url' + +const trpcClient = getTrpcClient() + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { title: 'Freshyo Admin' }, + ], + links: [ + { rel: 'stylesheet', href: appCss }, + { rel: 'icon', type: 'image/png', href: '/favicon.png' }, + ], + }), + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + +
{children}
+ + +
+
+
+
+ , + }, + ]} + /> + + + + ) +} diff --git a/apps/admin-web/src/routes/dashboard.banners.$id.edit.tsx b/apps/admin-web/src/routes/dashboard.banners.$id.edit.tsx new file mode 100644 index 0000000..9db67d1 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.banners.$id.edit.tsx @@ -0,0 +1,152 @@ +import React, { useState, useEffect } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { AppContainer, p as MyText } from 'web-components' +import { ArrowLeft } from 'lucide-react' +import BannerForm, { type BannerFormData } from '../components/BannerForm' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' + +export const Route = createFileRoute('/dashboard/banners/$id/edit')({ + component: EditBanner, +}) + +function EditBanner() { + const { id } = Route.useParams() + const bannerId = id as string + + const [isLoading, setIsLoading] = useState(true) + const [isSubmitting, setIsSubmitting] = useState(false) + // Load real banner data from API + const { data: bannerData } = trpc.admin.banner.getBanner.useQuery({ + id: parseInt(bannerId) + }) + const { refetch: refetchBanners } = trpc.admin.banner.getBanners.useQuery(undefined, { + enabled: false, + }) + const [banner, setBanner] = useState(undefined) + + + useEffect(() => { + const loadBanner = async () => { + try { + setIsLoading(true) + + + + if (bannerData) { + // Handle data format compatibility (productId -> skuIds migration) + const processedBanner = { + ...bannerData, + skuIds: Array.isArray(bannerData.skuIds) + ? bannerData.skuIds + : (bannerData.skuIds ? [bannerData.skuIds] : []) + } + + setBanner(processedBanner) + } else { + ErrorToast('Banner not found') + window.history.back() + } + } catch (err) { + console.error('Failed to load banner:', err) + ErrorToast('Failed to load banner data') + window.history.back() + } finally { + setIsLoading(false) + } + } + + loadBanner() + }, [bannerId, bannerData]) + + const initialValues: BannerFormData = banner ? { + name: banner.name, + imageUrl: banner.imageUrl, + description: banner.description || '', + skuIds: banner.skuIds || [], + redirectUrl: banner.redirectUrl || '', + // serialNum removed - handled automatically by backend + } : { + name: '', + imageUrl: '', + description: '', + skuIds: [], + redirectUrl: '', + // serialNum removed - handled automatically by backend + } + + const updateBannerMutation = trpc.admin.banner.updateBanner.useMutation() + + const handleSubmit = async (values: BannerFormData, imageUrl?: string) => { + if (!banner) return + + setIsSubmitting(true) + + try { + await updateBannerMutation.mutateAsync({ + id: banner.id, + name: values.name, + imageUrl: imageUrl || banner.imageUrl, + description: values.description || undefined, + skuIds: values.skuIds.length > 0 ? values.skuIds : [], + redirectUrl: values.redirectUrl || undefined, + }) + + await refetchBanners() + window.alert('Success: Banner updated successfully') + window.history.back() + } catch (err) { + ErrorToast('Failed to update banner. Please try again.') + } finally { + setIsSubmitting(false) + } + } + + const handleCancel = () => { + window.history.back() + } + + if (isLoading) { + return ( + +
+ Loading banner... +
+
+ ) + } + + if (!banner) { + return ( + +
+ Banner not found +
+
+ ) + } + + return ( + +
+ {/* Header */} +
+ + Edit Banner +
+
+ + +
+ + ) +} diff --git a/apps/admin-web/src/routes/dashboard.banners.new.tsx b/apps/admin-web/src/routes/dashboard.banners.new.tsx new file mode 100644 index 0000000..136bc55 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.banners.new.tsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { AppContainer, p as MyText } from 'web-components' +import { ArrowLeft } from 'lucide-react' +import BannerForm, { type BannerFormData } from '../components/BannerForm' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' + +export const Route = createFileRoute('/dashboard/banners/new')({ + component: CreateBanner, +}) + +function CreateBanner() { + const [isSubmitting, setIsSubmitting] = useState(false) + + const initialValues: BannerFormData = { + name: '', + imageUrl: '', + description: '', + skuIds: [], + redirectUrl: '', + // serialNum removed - assigned automatically by backend + } + + const createBannerMutation = trpc.admin.banner.createBanner.useMutation() + const { refetch: refetchBanners } = trpc.admin.banner.getBanners.useQuery(undefined, { + enabled: false, + }) + + const handleSubmit = async (values: BannerFormData, imageUrl?: string) => { + if (!imageUrl) { + ErrorToast('Image is required') + return + } + + setIsSubmitting(true) + + try { + await createBannerMutation.mutateAsync({ + name: values.name, + imageUrl, + description: values.description || undefined, + skuIds: values.skuIds.length > 0 ? values.skuIds : [], + redirectUrl: values.redirectUrl || undefined, + }) + + await refetchBanners() + window.alert('Success: Banner created successfully') + window.history.back() + } catch (err) { + ErrorToast('Failed to create banner. Please try again.') + } finally { + setIsSubmitting(false) + } + } + + const handleCancel = () => { + window.history.back() + } + + return ( + +
+ {/* Header */} +
+ + Create Banner +
+
+ + +
+ + ) +} diff --git a/apps/admin-web/src/routes/dashboard.banners.tsx b/apps/admin-web/src/routes/dashboard.banners.tsx new file mode 100644 index 0000000..3f173c2 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.banners.tsx @@ -0,0 +1,432 @@ +import React, { useState } from 'react' +import { createFileRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router' +import { AppContainer, p as MyText } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' +import { useManualRefresh } from '@/lib/refresh-context' +import { Image as ImageIcon, Plus, Pencil, Trash2, CircleCheck, CircleDot, Hourglass, X } from 'lucide-react' +import type { Banner as SharedBanner } from '@packages/shared' + +export const Route = createFileRoute('/dashboard/banners')({ + component: DashboardBanners, +}) + +// Serialized banner row (API JSON carries string dates) anchored to the shared shape +type Banner = Omit & { + createdAt: string, + lastUpdated: string +} + +function DashboardBanners() { + const location = useLocation() + const navigate = useNavigate() + const [refreshing, setRefreshing] = useState(false) + + // Edit mode state + const [editMode, setEditMode] = useState(false) + const [selectedSlot, setSelectedSlot] = useState(null) + const [slotAssignments, setSlotAssignments] = useState<{ [slot: number]: number | null }>({}) + const [originalAssignments, setOriginalAssignments] = useState<{ [slot: number]: number | null }>({}) + const [saving, setSaving] = useState(false) + + // Real API calls + const { data: bannersData, isLoading, error, refetch } = trpc.admin.banner.getBanners.useQuery() + + const deleteBannerMutation = trpc.admin.banner.deleteBanner.useMutation() + const updateBannerMutation = trpc.admin.banner.updateBanner.useMutation() + const emptySlotMutation = trpc.admin.banner.updateBanner.useMutation() + + const banners = bannersData?.banners || [] + + // Initialize slot assignments when banners load + React.useEffect(() => { + const assignments: { [slot: number]: number | null } = { 1: null, 2: null, 3: null, 4: null } + banners.forEach((banner) => { + if (banner.serialNum && banner.serialNum >= 1 && banner.serialNum <= 4) { + assignments[banner.serialNum] = banner.id + } + }) + setSlotAssignments(assignments) + setOriginalAssignments({ ...assignments }) + }, [bannersData]) + + const onRefresh = async () => { + setRefreshing(true) + await refetch() + setRefreshing(false) + } + + useManualRefresh(() => { + onRefresh() + }) + + if (location.pathname !== '/dashboard/banners') { + return + } + + // Slot and edit mode handlers + const handleSlotClick = (slotNumber: number) => { + setSelectedSlot(slotNumber) + setEditMode(true) + } + + const handleBannerSelect = (bannerId: number) => { + if (!editMode || selectedSlot === null) return + + // Remove banner from any existing slot + const newAssignments = { ...slotAssignments } + Object.keys(newAssignments).forEach((slot) => { + if (newAssignments[parseInt(slot)] === bannerId) { + newAssignments[parseInt(slot)] = null + } + }) + + // Assign banner to selected slot + newAssignments[selectedSlot] = bannerId + setSlotAssignments(newAssignments) + } + + const handleSave = async () => { + setSaving(true) + try { + // Get banners that need to be updated + const bannersToUpdate: { id: number, serialNum: number | null }[] = [] + + // Clear serial numbers for banners no longer in slots + banners.forEach((banner) => { + const currentSlot = banner.serialNum + const assignedSlot = Object.keys(slotAssignments).find((slot) => + slotAssignments[parseInt(slot)] === banner.id + ) + + if (currentSlot !== (assignedSlot ? parseInt(assignedSlot) : null)) { + bannersToUpdate.push({ + id: banner.id, + serialNum: assignedSlot ? parseInt(assignedSlot) : null + }) + } + }) + + // Update banners that gained slots + Object.keys(slotAssignments).forEach((slot) => { + const slotNum = parseInt(slot) + const bannerId = slotAssignments[slotNum] + if (bannerId) { + const banner = banners.find((b) => b.id === bannerId) + if (banner && banner.serialNum !== slotNum) { + bannersToUpdate.push({ + id: bannerId, + serialNum: slotNum + }) + } + } + }) + + // Execute updates + await Promise.all( + bannersToUpdate.map(({ id, serialNum }) => + updateBannerMutation.mutateAsync({ id, serialNum }) + ) + ) + + setOriginalAssignments({ ...slotAssignments }) + setEditMode(false) + setSelectedSlot(null) + await refetch() + window.alert('Success: Slot assignments saved successfully') + } catch (err) { + ErrorToast('Failed to save slot assignments') + } finally { + setSaving(false) + } + } + + const handleCancel = () => { + setSlotAssignments({ ...originalAssignments }) + setEditMode(false) + setSelectedSlot(null) + } + + const handleEmptySlot = async () => { + if (!selectedSlot || !slotAssignments[selectedSlot]) return + + const bannerId = slotAssignments[selectedSlot] + const banner = banners.find((b) => b.id === bannerId) + + if (!banner) return + + try { + // Update banner's serialNum to null to empty the slot + await emptySlotMutation.mutateAsync({ + id: banner.id, + serialNum: null + }) + + // Update local state + setSlotAssignments((prev) => ({ + ...prev, + [selectedSlot]: null + })) + + // Update original assignments for cancel functionality + setOriginalAssignments((prev) => ({ + ...prev, + [selectedSlot]: null + })) + + window.alert(`Success: Slot ${selectedSlot} has been emptied`) + } catch (err) { + ErrorToast('Failed to empty slot') + } + } + + // Helper function to get banner name by ID + const getBannerNameById = (id: number | null) => { + if (!id) return null + const banner = banners.find((b) => b.id === id) + return banner ? banner.name : null + } + + const handleEdit = (banner: Banner) => { + navigate({ to: '/dashboard/banners/$id/edit', params: { id: String(banner.id) } }) + } + + const handleDelete = (id: number) => { + if (!window.confirm('Delete Banner: Are you sure you want to delete this banner?')) return + ;(async () => { + try { + await deleteBannerMutation.mutateAsync({ id }) + refetch() + window.alert('Success: Banner deleted') + } catch (err) { + ErrorToast('Failed to delete banner') + } + })() + } + + const handleCreate = () => { + navigate({ to: '/dashboard/banners/new' }) + } + + if (isLoading) { + return ( + +
+ Loading banners... +
+
+ ) + } + + if (error) { + return ( + +
+ Error loading banners +
+
+ ) + } + + return ( + +
+
+ {/* Slots Row */} +
+ + {editMode ? `Select banner for Slot ${selectedSlot}` : 'Banner Slots'} + +
+ {[1, 2, 3, 4].map((slotNum) => { + const assignedBannerId = slotAssignments[slotNum] + const bannerName = getBannerNameById(assignedBannerId) + const isSelected = editMode && selectedSlot === slotNum + + return ( + + ) + })} +
+ + {/* Action buttons in edit mode */} + {editMode && ( +
+ {/* Save/Cancel buttons */} +
+ + +
+ + {/* Empty Slot button */} + +
+ )} +
+ + {/* Banners List */} +
+ {editMode && ( +
+ + Tap a banner below to assign it to Slot {selectedSlot} + +
+ )} + + {banners.length === 0 ? ( +
+ + + + No Banners Yet + + Start by creating your first banner using the button above. + +
+ ) : ( + banners.map((banner) => { + const isAssignedToSlot = Object.values(slotAssignments).includes(banner.id) + const isAssignedToSelectedSlot = slotAssignments[selectedSlot || 0] === banner.id + const canInteract = editMode + + return ( +
handleBannerSelect(banner.id) : undefined} + role={canInteract ? 'button' : undefined} + tabIndex={canInteract ? 0 : undefined} + onKeyDown={canInteract ? (e) => { if (e.key === 'Enter') handleBannerSelect(banner.id) } : undefined} + className={`mb-3 w-full rounded-xl border bg-white p-4 text-left shadow-sm ${canInteract ? 'cursor-pointer' : ''} ${canInteract && isAssignedToSelectedSlot + ? 'border-blue-500 bg-blue-50' + : canInteract && isAssignedToSlot + ? 'border-green-300 bg-green-50' + : canInteract + ? 'border-gray-200' + : 'border-gray-100' + }`} + > + + {banner.name} + + + + {banner.name} + + + {canInteract && isAssignedToSelectedSlot && ( + + + + )} + {canInteract && isAssignedToSlot && !isAssignedToSelectedSlot && ( + + + + )} + + {!editMode && ( + <> + + + + )} + + + {banner.description && ( + + {banner.description} + + )} + + + Created: {new Date(banner.createdAt).toLocaleDateString()} + + {banner.serialNum && banner.serialNum >= 1 && banner.serialNum <= 4 && ( + + Slot {banner.serialNum} + + )} + + + +
+ ) + }) + )} +
+
+ + {/* Floating Action Button */} + +
+
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.complaints.tsx b/apps/admin-web/src/routes/dashboard.complaints.tsx new file mode 100644 index 0000000..13acd4f --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.complaints.tsx @@ -0,0 +1,314 @@ +import React, { useState, useCallback, useMemo } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { CircleAlert, Reply, User as UserIcon, ShoppingBag, Inbox } from 'lucide-react' +import { p as MyText, MyFlatList, MyButton, ConfirmationDialog } from 'web-components' +import { useMarkDataFetchers } from '@/lib/refresh-context' +import { trpc } from '@/lib/trpc-client' + +export const Route = createFileRoute('/dashboard/complaints')({ + component: Complaints, +}) + +function Complaints() { + const navigate = useNavigate() + const [dialogOpen, setDialogOpen] = useState(false) + const [selectedComplaintId, setSelectedComplaintId] = useState(null) + const [viewerOpen, setViewerOpen] = useState(false) + const [viewerImages, setViewerImages] = useState([]) + const [viewerIndex, setViewerIndex] = useState(0) + + const { + data, + isLoading, + isError, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + refetch, + } = trpc.admin.complaint.getAll.useInfiniteQuery( + { limit: 20 }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + } + ) + + useMarkDataFetchers(() => { + refetch() + }) + + const resolveComplaint = trpc.admin.complaint.resolve.useMutation() + + const complaints = useMemo(() => { + const allComplaints = data?.pages.flatMap((page) => page.complaints) || [] + return allComplaints.filter( + (complaint, index, self) => + index === self.findIndex((c) => c.id === complaint.id) + ) + }, [data]) + + const handleLoadMore = useCallback(() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }, [hasNextPage, isFetchingNextPage, fetchNextPage]) + + const handleUserPress = useCallback((userId: number) => { + navigate({ to: '/dashboard/users/$id', params: { id: String(userId) } }) + }, [navigate]) + + const handleOrderPress = useCallback((orderId: number) => { + navigate({ to: '/dashboard/orders/$id', params: { id: String(orderId) } }) + }, [navigate]) + + const handleMarkResolved = (id: number) => { + setSelectedComplaintId(id) + setDialogOpen(true) + } + + const handleConfirmResolve = (response?: string) => { + if (!selectedComplaintId) return + + resolveComplaint.mutate( + { id: String(selectedComplaintId), response: response ?? '' }, + { + onSuccess: () => { + window.alert('Success: Complaint marked as resolved') + refetch() + setDialogOpen(false) + setSelectedComplaintId(null) + }, + onError: (error: any) => { + window.alert(`Error: ${error.message || 'Failed to resolve complaint'}`) + setDialogOpen(false) + setSelectedComplaintId(null) + }, + } + ) + } + + const openImageViewer = (images: string[], index: number) => { + setViewerImages(images) + setViewerIndex(index) + setViewerOpen(true) + } + + if (isLoading) { + return ( +
+
+ Loading complaints... +
+ ) + } + + if (isError) { + return ( +
+ + Error + + {error?.message || 'Failed to load complaints'} + + +
+ ) + } + + return ( +
+
+ item.id.toString()} + renderItem={(item) => ( +
+
+ + Complaint #{item.id} + + + + {item.status === 'resolved' ? 'Resolved' : 'Pending'} + + +
+ + + {item.text} + + + {item.response && ( +
+
+ + + Admin Reply + +
+ + {item.response} + +
+ )} + + {item.images && item.images.length > 0 && ( +
+ + Attached Images: + +
+ {item.images.map((imageUri: string, index: number) => ( + {`Complaint openImageViewer(item.images as string[], index)} + className="h-16 w-16 cursor-pointer rounded-lg border border-gray-200 object-cover" + /> + ))} +
+
+ )} + +
+ + + {item.orderId && ( + <> + | + + + + )} +
+ + {item.status === 'pending' && ( + + )} +
+ )} + ListEmptyComponent={ +
+
+ +
+ + No complaints + + + All complaints will appear here + +
+ } + ListFooterComponent={ + isFetchingNextPage ? ( +
+
+ Loading more... +
+ ) : hasNextPage ? ( +
+ +
+ ) : null + } + /> +
+ { + setDialogOpen(false) + setSelectedComplaintId(null) + }} + title="Resolve Complaint" + message="Add admin notes for this resolution:" + confirmText="Resolve" + cancelText="Cancel" + isLoading={resolveComplaint.isPending} + /> + {viewerOpen && viewerImages.length > 0 && ( +
setViewerOpen(false)}> + + {viewerImages.length > 1 && ( + + )} + Complaint attachment viewer e.stopPropagation()} + /> + {viewerImages.length > 1 && ( + + )} +
+ {viewerIndex + 1} / {viewerImages.length} +
+
+ )} +
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.coupons.$id.edit.tsx b/apps/admin-web/src/routes/dashboard.coupons.$id.edit.tsx new file mode 100644 index 0000000..8f61633 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.coupons.$id.edit.tsx @@ -0,0 +1,126 @@ +import React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { AppContainer, p as MyText } from 'web-components' +import CouponForm from '../components/CouponForm' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' +import dayjs from 'dayjs' + +// Ported from common-ui/shared-types (CreateCouponPayload) — @packages/shared +// does not export it, so the shape is copied here to keep behavior identical. +interface CreateCouponPayload { + couponCodes: string[], + discountPercent?: number, + flatDiscount?: number, + minOrder?: number, + maxValue?: number, + validTill?: string, + maxLimitForUser?: number, + isApplyForAll: boolean, + isUserBased: boolean, + targetUsers?: number[], + productIds?: number[], + skuIds?: number[], + applicableUsers?: number[], + applicableProducts?: number[], + exclusiveApply?: boolean +} + +export const Route = createFileRoute('/dashboard/coupons/$id/edit')({ + component: EditCoupon, +}) + +function EditCoupon() { + const { id } = Route.useParams() + const couponId = parseInt(id as string) + + const { data: coupon, isLoading, refetch } = trpc.admin.coupon.getById.useQuery({ id: couponId }) + const { refetch: refetchCoupons } = trpc.admin.coupon.getAll.useInfiniteQuery( + { limit: 20, search: '' }, + { + enabled: false, + getNextPageParam: (lastPage) => lastPage.nextCursor, + } + ) + const { refetch: refetchReservedCoupons } = trpc.admin.coupon.getReservedCoupons.useInfiniteQuery( + { limit: 20, search: '' }, + { + enabled: false, + getNextPageParam: (lastPage) => lastPage.nextCursor, + } + ) + const updateCoupon = trpc.admin.coupon.update.useMutation() + + const handleUpdateCoupon = (values: CreateCouponPayload & { isReservedCoupon?: boolean }) => { + // Transform targetUsers array to targetUser for backend compatibility + const { targetUsers, ...rest } = values + const updates = { + ...rest, + targetUser: targetUsers?.[0] || undefined, + } + + // For flat discounts, send the discount value as maxValue too + if (updates.flatDiscount !== undefined) { + updates.maxValue = updates.flatDiscount + } + + updateCoupon.mutate({ id: couponId, updates }, { + onSuccess: async () => { + await refetch() + await refetchCoupons() + await refetchReservedCoupons() + window.alert('Success: Coupon updated successfully') + window.history.back() + }, + onError: (error: any) => { + ErrorToast(error.message || 'Failed to update coupon') + }, + }) + } + + if (isLoading) { + return ( + +
+ Loading... +
+
+ ) + } + + if (!coupon) { + return ( + +
+ Coupon not found +
+
+ ) + } + + // Transform coupon data for form (targetUser to targetUsers array) + const initialValues: Partial = { + couponCodes: coupon.couponCode ? [coupon.couponCode] : [''], + isUserBased: coupon.isUserBased, + isApplyForAll: coupon.isApplyForAll, + targetUsers: coupon.targetUser ? [coupon.targetUser.id] : [], + discountPercent: coupon.discountPercent ? parseFloat(coupon.discountPercent) : undefined, + flatDiscount: coupon.flatDiscount ? parseFloat(coupon.flatDiscount) : undefined, + minOrder: coupon.minOrder ? parseFloat(coupon.minOrder) : undefined, + maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined, + validTill: coupon.validTill ? dayjs(coupon.validTill).format('YYYY-MM-DD') : undefined, + maxLimitForUser: coupon.maxLimitForUser || undefined, + skuIds: coupon.skuIds, + isReservedCoupon: false, // Normal coupons + } + + return ( + + + + ) +} diff --git a/apps/admin-web/src/routes/dashboard.coupons.new.tsx b/apps/admin-web/src/routes/dashboard.coupons.new.tsx new file mode 100644 index 0000000..3fe7347 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.coupons.new.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { AppContainer } from 'web-components' +import CouponForm from '../components/CouponForm' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' + +export const Route = createFileRoute('/dashboard/coupons/new')({ + component: CreateCoupon, +}) + +function CreateCoupon() { + const createCoupon = trpc.admin.coupon.create.useMutation() + const createReservedCoupon = trpc.admin.coupon.createReservedCoupon.useMutation() + const { refetch: refetchCoupons } = trpc.admin.coupon.getAll.useInfiniteQuery( + { limit: 20, search: '' }, + { + enabled: false, + getNextPageParam: (lastPage) => lastPage.nextCursor, + } + ) + const { refetch: refetchReservedCoupons } = trpc.admin.coupon.getReservedCoupons.useInfiniteQuery( + { limit: 20, search: '' }, + { + enabled: false, + getNextPageParam: (lastPage) => lastPage.nextCursor, + } + ) + + const handleCreateCoupon = async (values: any) => { + const { isReservedCoupon, couponCodes, ...rest } = values + // Transform targetUsers array to targetUser for backend compatibility + const payload = { + ...rest, + targetUser: rest.targetUsers?.[0] || undefined, + } + delete payload.targetUsers + + // For flat discounts, send the discount value as maxValue too + if (payload.flatDiscount !== undefined) { + payload.maxValue = payload.flatDiscount + } + + const codes = (couponCodes || []).map((c: string) => c.trim()).filter(Boolean) + if (codes.length === 0) { + ErrorToast('At least one coupon code is required') + return + } + + const mutation = isReservedCoupon ? createReservedCoupon : createCoupon + try { + await mutation.mutateAsync({ ...payload, couponCodes: codes }) + await refetchCoupons() + await refetchReservedCoupons() + window.alert(`Success: ${codes.length} coupon${codes.length > 1 ? 's' : ''} created successfully`) + window.history.back() + } catch (error: any) { + ErrorToast(error.message || 'Failed to create coupons') + } + } + + return ( + + + + ) +} diff --git a/apps/admin-web/src/routes/dashboard.coupons.tsx b/apps/admin-web/src/routes/dashboard.coupons.tsx new file mode 100644 index 0000000..53966ef --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.coupons.tsx @@ -0,0 +1,509 @@ +import React, { useState, useCallback, useMemo } from 'react' +import { createFileRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router' +import { Percent, IndianRupee, Pencil, Trash2, TicketPercent, CircleAlert, Loader2, Users, Package, UserCheck, User, ListFilter, Plus } from 'lucide-react' +import { p as MyText, MyButton, SearchBar, MyFlatList } from 'web-components' +import { useManualRefresh, useMarkDataFetchers } from '@/lib/refresh-context' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' +import { MultiSelect } from '@/components/MultiSelect' + +export const Route = createFileRoute('/dashboard/coupons')({ + component: Coupons, +}) + +const CouponItem = ({ item, onDelete }: { item: any, onDelete: (id: number) => void }) => { + const navigate = useNavigate() + + const getCouponStatus = (coupon: any) => { + if (coupon.isInvalidated) return 'inactive' + if (coupon.validTill && new Date(coupon.validTill) <= new Date()) return 'expired' + return 'active' + } + + const status = getCouponStatus(item) + + const getBorderColor = () => { + if (status === 'active') return 'border-green-500' + if (status === 'expired') return 'border-yellow-500' + return 'border-red-500' + } + + const getBgColor = () => { + if (status === 'active') return 'bg-green-100' + if (status === 'expired') return 'bg-yellow-100' + return 'bg-red-100' + } + + const getTextColor = () => { + if (status === 'active') return 'text-green-600' + if (status === 'expired') return 'text-yellow-600' + return 'text-red-600' + } + + const getIconColor = () => { + if (status === 'active') return '#10b981' + if (status === 'expired') return '#f59e0b' + return '#ef4444' + } + + return ( +
+
+ + {item.discountPercent ? ( + + ) : ( + + )} + +
+ {item.couponCode} + ID: {item.id} +
+ + + {status === 'active' ? 'Active' : status === 'expired' ? 'Expired' : 'Inactive'} + + +
+ +
+ + Discount: {item.discountPercent ? `${item.discountPercent}% off` : item.flatDiscount ? `₹${item.flatDiscount} off` : 'N/A'} + +
+ Min Order: {item.minOrder ? `₹${item.minOrder}` : 'None'} + Max: {item.maxValue ? `₹${item.maxValue}` : 'None'} +
+ + Valid Till: {item.validTill ? new Date(item.validTill).toLocaleDateString() : 'No expiry'} + +
+ +
+ + Target: {item.isApplyForAll ? 'All Users' : item.applicableUsers?.length > 0 ? `${item.applicableUsers.length} Users` : 'All Users'} + + {item.applicableProducts && item.applicableProducts.length > 0 && ( + + Products: {item.applicableProducts.length} selected + + )} +
+ +
+ + +
+
+ ) +} + +const ReservedCouponItem = ({ item }: { item: any }) => { + const getStatus = () => { + if (item.isRedeemed) return 'redeemed' + if (item.validTill && new Date(item.validTill) <= new Date()) return 'expired' + return 'active' + } + + const status = getStatus() + + const getBorderColor = () => { + if (status === 'active') return 'border-green-500' + if (status === 'expired') return 'border-yellow-500' + return 'border-blue-500' + } + + const getBgColor = () => { + if (status === 'active') return 'bg-green-100' + if (status === 'expired') return 'bg-yellow-100' + return 'bg-blue-100' + } + + const getTextColor = () => { + if (status === 'active') return 'text-green-600' + if (status === 'expired') return 'text-yellow-600' + return 'text-blue-600' + } + + const getIconColor = () => { + if (status === 'active') return '#10b981' + if (status === 'expired') return '#f59e0b' + return '#3b82f6' + } + + return ( +
+
+ + {item.discountPercent ? ( + + ) : ( + + )} + +
+ {item.secretCode} + Coupon: {item.couponCode} +
+ + + {status === 'active' ? 'Active' : status === 'expired' ? 'Expired' : 'Redeemed'} + + +
+ +
+ + Discount: {item.discountPercent ? `${item.discountPercent}% off` : item.flatDiscount ? `₹${item.flatDiscount} off` : 'N/A'} + +
+ Min Order: {item.minOrder ? `₹${item.minOrder}` : 'None'} + Max: {item.maxValue ? `₹${item.maxValue}` : 'None'} +
+ + Valid Till: {item.validTill ? new Date(item.validTill).toLocaleDateString() : 'No expiry'} + +
+ + {item.isRedeemed && item.redeemedUser && ( +
+ + Redeemed by: {item.redeemedUser.name || 'Unknown'} ({item.redeemedUser.mobile}) + + + Redeemed on: {item.redeemedAt ? new Date(item.redeemedAt).toLocaleDateString() : 'N/A'} + +
+ )} + +
+ + Created by: {item.creator?.name || 'Unknown'} + +
+
+ ) +} + +const GeneralTab = () => { + const [refreshing, setRefreshing] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [statusFilters, setStatusFilters] = useState([]) + const { + data, + fetchNextPage, + hasNextPage, + isLoading, + isFetchingNextPage, + error, + refetch, + } = trpc.admin.coupon.getAll.useInfiniteQuery( + { limit: 20, search: searchQuery }, + { getNextPageParam: (lastPage) => lastPage.nextCursor } + ) + + const coupons = useMemo(() => data?.pages.flatMap((page) => page.coupons) || [], [data]) + + const getCouponStatus = (coupon: any) => { + if (coupon.isInvalidated) return 'inactive' + if (coupon.validTill && new Date(coupon.validTill) <= new Date()) return 'expired' + return 'active' + } + + const filteredCoupons = useMemo(() => { + let filtered = coupons + if (statusFilters.length > 0) { + filtered = filtered.filter((coupon) => statusFilters.includes(getCouponStatus(coupon))) + } + return filtered + }, [coupons, statusFilters]) + + const handleRefresh = useCallback(async () => { + setRefreshing(true) + await refetch() + setRefreshing(false) + }, [refetch]) + + useManualRefresh(() => { refetch() }) + + useMarkDataFetchers(() => { + refetch() + }) + + const deleteCoupon = trpc.admin.coupon.delete.useMutation() + + const handleDeleteCoupon = (id: number) => { + if (!window.confirm('Delete Coupon: Are you sure you want to delete this coupon?')) return + deleteCoupon.mutate({ id }, { + onSuccess: () => { + window.alert('Success: Coupon deleted successfully') + refetch() + }, + onError: (error: any) => { + ErrorToast(error.message || 'Failed to delete coupon') + }, + }) + } + + if (isLoading) { + return ( +
+ + Loading Coupons... +
+ ) + } + + if (error) { + return ( +
+ + Failed to load coupons + refetch()} variant="red"> + Retry + +
+ ) + } + + return ( +
+
+
+ +
+ setStatusFilters(value as string[])} + multiple={true} + triggerComponent={({ onClick }: any) => ( + + )} + /> +
+ item.id.toString()} + renderItem={(item) => } + onRefresh={handleRefresh} + refreshing={refreshing} + className="px-4 pb-4" + ListFooterComponent={ +
+ {isFetchingNextPage ? ( + Loading more... + ) : hasNextPage ? ( + fetchNextPage()}> + Load more + + ) : null} +
+ } + ListEmptyComponent={ +
+ + No Coupons Found + {searchQuery ? ( + setSearchQuery('')} variant="blue" className="mt-4 bg-gray-500"> + Clear Search + + ) : null} +
+ } + /> +
+ ) +} + +const ReservedTab = () => { + const [refreshing, setRefreshing] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [statusFilters, setStatusFilters] = useState([]) + + const { + data, + isLoading, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + error, + refetch + } = trpc.admin.coupon.getReservedCoupons.useInfiniteQuery( + { limit: 20, search: searchQuery }, + { getNextPageParam: (lastPage) => lastPage.nextCursor } + ) + + const coupons = useMemo(() => data?.pages.flatMap((page) => page.coupons) || [], [data]) + + const getStatus = (coupon: any) => { + if (coupon.isRedeemed) return 'redeemed' + if (coupon.validTill && new Date(coupon.validTill) <= new Date()) return 'expired' + return 'active' + } + + const filteredCoupons = useMemo(() => { + let filtered = coupons + if (statusFilters.length > 0) { + filtered = filtered.filter((coupon) => statusFilters.includes(getStatus(coupon))) + } + return filtered + }, [coupons, statusFilters]) + + const handleRefresh = useCallback(async () => { + setRefreshing(true) + await refetch() + setRefreshing(false) + }, [refetch]) + + useManualRefresh(() => { refetch() }) + + useMarkDataFetchers(() => { + refetch() + }) + + if (isLoading) { + return ( +
+ + Loading Reserved Coupons... +
+ ) + } + + if (error) { + return ( +
+ + Failed to load reserved coupons + refetch()} variant="red"> + Retry + +
+ ) + } + + return ( +
+
+
+ +
+ setStatusFilters(value as string[])} + multiple={true} + triggerComponent={({ onClick }: any) => ( + + )} + /> +
+ item.id.toString()} + renderItem={(item) => } + onRefresh={handleRefresh} + refreshing={refreshing} + className="px-4 pb-4" + ListFooterComponent={ +
+ {isFetchingNextPage ? ( + Loading more... + ) : hasNextPage ? ( + fetchNextPage()}> + Load more + + ) : null} +
+ } + ListEmptyComponent={ +
+ + No Reserved Coupons Found + {searchQuery ? ( + setSearchQuery('')} variant="blue" className="mt-4 bg-gray-500"> + Clear Search + + ) : null} +
+ } + /> +
+ ) +} + +function Coupons() { + const location = useLocation() + const navigate = useNavigate() + const [tab, setTab] = useState<'general' | 'reserved'>('general') + + if (location.pathname !== '/dashboard/coupons') { + return + } + + return ( +
+
+ {([ + { key: 'general', title: 'General' }, + { key: 'reserved', title: 'Reserved' }, + ] as const).map((r) => ( + + ))} +
+
+ {tab === 'general' ? : } +
+ +
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.customize-app.ordering.tsx b/apps/admin-web/src/routes/dashboard.customize-app.ordering.tsx new file mode 100644 index 0000000..c153e41 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.customize-app.ordering.tsx @@ -0,0 +1,301 @@ +import React, { useState, useEffect, useCallback } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { useQueryClient } from '@tanstack/react-query' +import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core' +import { SortableContext, useSortable, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { GripVertical, Image as ImageIcon, XCircle, ChevronLeft, CircleAlert, Package } from 'lucide-react' +import { AppContainer, p as MyText } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import type { StoreProductCard } from '@packages/shared' +import type { DragOrderItemProps } from '@/types/drag-order-item' + +export const Route = createFileRoute('/dashboard/customize-app/ordering')({ + component: AllItemsOrder, +}) + +type Product = Pick + +const itemStyle: React.CSSProperties = { + width: '100%', + height: 60, + backgroundColor: 'white', + borderRadius: 8, + borderWidth: 1, + borderStyle: 'solid', + borderColor: '#e5e7eb', + padding: 10, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + boxShadow: '0 1px 2px rgba(0,0,0,0.1)', + marginTop: 4, + marginBottom: 4, +} + +const activeItemStyle: React.CSSProperties = { + ...itemStyle, + borderColor: '#3b82f6', + boxShadow: '0 4px 8px rgba(59,130,246,0.3)', +} + +const ProductItem: React.FC> = ({ + item, + isActive, +}) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.id }) + const active = isActive || isDragging + + return ( +
+
+ +
+ + {item.images?.[0] ? ( + {item.name} + ) : ( +
+ +
+ )} + +
+ + {item.name.length > 30 ? item.name.substring(0, 30) + '...' : item.name} + + + {item.isOutOfStock && ( + + )} +
+
+ ) +} + +function AllItemsOrder() { + const queryClient = useQueryClient() + const [products, setProducts] = useState([]) + const [hasChanges, setHasChanges] = useState(false) + + const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery() + const { data: allProducts, isLoading: isLoadingProducts, error: productsError } = trpc.common.product.getAllProductsSummary.useQuery() + const updateConstants = trpc.admin.const.updateConstants.useMutation() + + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })) + + useEffect(() => { + if (allProducts?.products) { + const allItemsOrderConstant = constants?.find((c) => c.key === 'allItemsOrder') + + let orderedIds: number[] = [] + + if (allItemsOrderConstant) { + const value = allItemsOrderConstant.value + + if (Array.isArray(value)) { + orderedIds = value.map((id: any) => parseInt(id)) + } else if (typeof value === 'string') { + orderedIds = value.split(',').map((id: string) => parseInt(id.trim())).filter((id) => !isNaN(id)) + } + } + + const productMap = new Map(allProducts.products.map((p) => [p.id, p])) + + const sortedProducts: Product[] = [] + + for (const id of orderedIds) { + const product = productMap.get(id) + if (product) { + sortedProducts.push({ + id: product.id, + name: product.name, + images: product.images || [], + isOutOfStock: product.isOutOfStock || false, + }) + productMap.delete(id) + } + } + + for (const product of productMap.values()) { + sortedProducts.push({ + id: product.id, + name: product.name, + images: product.images || [], + isOutOfStock: product.isOutOfStock || false, + }) + } + + setProducts(sortedProducts) + } + }, [constants, allProducts]) + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event + if (over && active.id !== over.id) { + setProducts((prev) => { + const oldIndex = prev.findIndex((p) => p.id === active.id) + const newIndex = prev.findIndex((p) => p.id === over.id) + return arrayMove(prev, oldIndex, newIndex) + }) + setHasChanges(true) + } + }, []) + + const handleSave = () => { + const productIds = products.map((p) => p.id) + + updateConstants.mutate( + { + constants: [{ + key: 'allItemsOrder', + value: productIds, + }], + }, + { + onSuccess: () => { + setHasChanges(false) + window.alert('All items order updated successfully!') + queryClient.invalidateQueries({ queryKey: ['const.getConstants'] }) + }, + onError: (error) => { + window.alert('Failed to update items order. Please try again.') + console.error('Update all items order error:', error) + }, + } + ) + } + + if (isLoadingConstants || isLoadingProducts) { + return ( + +
+
+ + All Items Order +
+
+
+
+ + {isLoadingConstants ? 'Loading order...' : 'Loading products...'} + +
+
+ + ) + } + + if (constantsError || productsError) { + return ( + +
+
+ + All Items Order +
+
+
+ + Error + + {constantsError ? 'Failed to load order' : 'Failed to load products'} + + +
+
+ + ) + } + + return ( +
+
+ + + All Items Order + + +
+ + {products.length === 0 ? ( +
+ + + No products available + +
+ ) : ( +
+
+ + Long press and drag to reorder • {products.length} items + +
+ +
+ + p.id)} strategy={verticalListSortingStrategy}> + {products.map((product) => ( + {}} isActive={false} /> + ))} + + +
+
+ )} +
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.customize-app.popular.tsx b/apps/admin-web/src/routes/dashboard.customize-app.popular.tsx new file mode 100644 index 0000000..0d4cad8 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.customize-app.popular.tsx @@ -0,0 +1,413 @@ +import React, { useState, useEffect } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { useQueryClient } from '@tanstack/react-query' +import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core' +import { SortableContext, useSortable, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { GripVertical, Image as ImageIcon, Trash2, ChevronLeft, CircleAlert, Package, Plus } from 'lucide-react' +import { AppContainer, p as MyText, BottomDialog } from 'web-components' +import ProductsSelector from '../components/ProductsSelector' +import { trpc } from '@/lib/trpc-client' +import type { StoreProductCard } from '@packages/shared' +import type { DragOrderItemProps } from '@/types/drag-order-item' + +export const Route = createFileRoute('/dashboard/customize-app/popular')({ + component: CustomizePopularItems, +}) + +type PopularProduct = Omit< + StoreProductCard, + 'price' | 'marketPrice' | 'unitNotation' | 'images' +> & { + price: number + marketPrice: number | null + images: string[] | null + storeId: number | null + nextDeliveryDate: string | null +} + +interface ProductItemProps extends DragOrderItemProps { + onDelete: (id: number) => void +} + +const ProductItem: React.FC = ({ + item, + isActive, + onDelete, +}) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.id }) + const active = isActive || isDragging + + return ( +
+
+
+
+ +
+ + {item.images?.[0] ? ( + {item.name} + ) : ( +
+ +
+ )} + +
+ + {item.name} + + + ₹{item.price} + +
+ + +
+
+
+ ) +} + +function CustomizePopularItems() { + const queryClient = useQueryClient() + const [popularProducts, setPopularProducts] = useState([]) + const [hasChanges, setHasChanges] = useState(false) + const [showAddDialog, setShowAddDialog] = useState(false) + const [selectedProductIds, setSelectedProductIds] = useState([]) + + const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery() + const { data: allProducts, isLoading: isLoadingProducts, error: productsError } = trpc.common.product.getAllProductsSummary.useQuery() + const updateConstants = trpc.admin.const.updateConstants.useMutation() + + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })) + + useEffect(() => { + if (constants && allProducts?.products) { + const popularItemsConstant = constants.find((c) => c.key === 'popularItems') + + let popularIds: number[] = [] + + if (popularItemsConstant) { + const value = popularItemsConstant.value + + if (Array.isArray(value)) { + popularIds = value.map((id: any) => parseInt(id)) + } else if (typeof value === 'string') { + popularIds = value.split(',').map((id: string) => parseInt(id.trim())).filter((id) => !isNaN(id)) + } + + const popularProds: PopularProduct[] = [] + + for (const id of popularIds) { + const product = allProducts.products.find((p) => p.id === id) + if (product) { + popularProds.push(product as PopularProduct) + } + } + + setPopularProducts(popularProds) + } + } + }, [constants, allProducts]) + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event + if (over && active.id !== over.id) { + setPopularProducts((prev) => { + const oldIndex = prev.findIndex((p) => p.id === active.id) + const newIndex = prev.findIndex((p) => p.id === over.id) + return arrayMove(prev, oldIndex, newIndex) + }) + setHasChanges(true) + } + } + + const handleDelete = (productId: number) => { + if (window.confirm('Are you sure you want to remove this product from popular items?')) { + setPopularProducts((prev) => prev.filter((p) => p.id !== productId)) + setHasChanges(true) + } + } + + const handleAddProduct = () => { + if (selectedProductIds.length > 0) { + const newProducts = selectedProductIds + .map((id) => allProducts?.products.find((p) => p.id === id)) + .filter((product): product is NonNullable => + product !== undefined && !popularProducts.find((p) => p.id === product.id) + ) + + if (newProducts.length > 0) { + setPopularProducts((prev) => [...prev, ...newProducts as PopularProduct[]]) + setHasChanges(true) + } + + setSelectedProductIds([]) + setShowAddDialog(false) + } + } + + const handleSave = () => { + const popularIds = popularProducts.map((p) => p.id) + + updateConstants.mutate( + { + constants: [{ + key: 'popularItems', + value: popularIds, + }], + }, + { + onSuccess: () => { + setHasChanges(false) + window.alert('Popular items updated successfully!') + queryClient.invalidateQueries({ queryKey: ['const.getConstants'] }) + }, + onError: (error) => { + window.alert('Failed to update popular items. Please try again.') + console.error('Update popular items error:', error) + }, + } + ) + } + + const availableProducts = (allProducts?.products || []).filter( + (product) => !popularProducts.find((p) => p.id === product.id) + ) + + if (isLoadingConstants || isLoadingProducts) { + return ( + +
+
+ + + Popular Items + +
+
+ +
+
+ + {isLoadingConstants ? 'Loading constants...' : 'Loading products...'} + +
+
+ + ) + } + + if (constantsError || productsError) { + return ( + +
+
+ + + Popular Items + +
+
+ +
+ + Error + + {constantsError ? 'Failed to load constants' : 'Failed to load products'} + + +
+
+ + ) + } + + return ( +
+
+ + + Popular Items + + +
+ + {popularProducts.length === 0 ? ( +
+ + + No popular items configured + + + Add products to display as popular items + +
+ ) : ( +
+
+ + Long press an item to drag and reorder + +
+ + + p.id)} strategy={verticalListSortingStrategy}> + {popularProducts.map((product) => ( + {}} + isActive={false} + onDelete={handleDelete} + /> + ))} + + +
+ )} + +
+ +
+ + { + setShowAddDialog(false) + setSelectedProductIds([]) + }} + > +
+
+
+ + Add Popular Items + + + Select products to add to popular items + +
+ + {availableProducts.length === 0 ? ( +
+ + + All products are already in popular items + +
+ ) : ( + <> + setSelectedProductIds(val as number[])} + multiple={true} + label="Select Products" + placeholder="Choose products..." + labelFormat={(product) => `${product.productName} - ₹${product.price}`} + /> + +
+ + + +
+ + )} +
+ +
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.customize-app.tsx b/apps/admin-web/src/routes/dashboard.customize-app.tsx new file mode 100644 index 0000000..ef9a36b --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.customize-app.tsx @@ -0,0 +1,271 @@ +import React from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { Formik } from 'formik' +import { Pencil, ChevronRight, ArrowDownUp } from 'lucide-react' +import { p as MyText, pInput as MyTextInput, AppContainer, Checkbox } from 'web-components' +import { trpc } from '@/lib/trpc-client' + +export const Route = createFileRoute('/dashboard/customize-app')({ + component: CustomizeApp, +}) + +type ConstantFormData = Record + +const CONST_LABELS: Record = { + minRegularOrderValue: 'Minimum Regular Order Value', + freeDeliveryThreshold: 'Free Delivery Threshold', + deliveryCharge: 'Delivery Charge', + flashFreeDeliveryThreshold: 'Flash Free Delivery Threshold', + flashDeliveryCharge: 'Flash Delivery Charge', + platformFeePercent: 'Platform Fee Percent', + taxRate: 'Tax Rate', + minOrderAmountForCoupon: 'Minimum Order Amount for Coupon', + maxCouponDiscount: 'Maximum Coupon Discount', + flashDeliverySlotId: 'Flash Delivery Slot ID', + readableOrderId: 'Readable Order ID', + versionNum: 'Version Number', + playStoreUrl: 'Play Store URL', + appStoreUrl: 'App Store URL', + popularItems: 'Popular Items', + allItemsOrder: 'All Items Order', + isFlashDeliveryEnabled: 'Enable Flash Delivery', + supportMobile: 'Support Mobile', + supportEmail: 'Support Email', +} + +const CONST_VISIBILITY: Record = { + minRegularOrderValue: true, + freeDeliveryThreshold: true, + deliveryCharge: true, + flashFreeDeliveryThreshold: true, + flashDeliveryCharge: true, + platformFeePercent: true, + taxRate: false, + minOrderAmountForCoupon: true, + maxCouponDiscount: false, + flashDeliverySlotId: true, + readableOrderId: false, + versionNum: true, + playStoreUrl: true, + appStoreUrl: true, + popularItems: true, + allItemsOrder: true, + isFlashDeliveryEnabled: true, + supportMobile: true, + supportEmail: true, + tester: false, +} + +interface ConstantInputProps { + constantKey: string + value: any + setFieldValue: (field: string, value: any) => void + router: any +} + +const ConstantInput: React.FC = ({ constantKey, value, setFieldValue, router }) => { + const fieldName = constantKey + + if (constantKey === 'popularItems') { + return ( +
+ + {CONST_LABELS[constantKey] || constantKey} + + +
+ ) + } + + if (constantKey === 'allItemsOrder') { + return ( +
+ + {CONST_LABELS[constantKey] || constantKey} + + +
+ ) + } + + if (typeof value === 'boolean') { + return ( +
+ + {CONST_LABELS[constantKey] || constantKey} + +
+ setFieldValue(fieldName, !value)} + size={28} + /> + + {value ? 'Enabled' : 'Disabled'} + +
+
+ ) + } + + if (typeof value === 'number') { + return ( + { + const numValue = parseFloat(e.target.value) + setFieldValue(fieldName, isNaN(numValue) ? 0 : numValue) + }} + type="number" + placeholder="Enter number" + /> + ) + } + + if (Array.isArray(value)) { + return ( + { + const arrayValue = e.target.value.split(',').map((s) => s.trim()).filter((s) => s.length > 0) + setFieldValue(fieldName, arrayValue) + }} + placeholder="Enter comma separated values" + /> + ) + } + + return ( + { + setFieldValue(fieldName, e.target.value) + }} + placeholder="Enter value" + /> + ) +} + +function CustomizeApp() { + const navigate = useNavigate() + const router = { + push: (to: string) => navigate({ to: to as any }), + } + const { data: constants, isLoading: isLoadingConstants, refetch } = trpc.admin.const.getConstants.useQuery() + const { mutate: updateConstants, isPending: isUpdating } = trpc.admin.const.updateConstants.useMutation() + + const handleSubmit = (values: ConstantFormData) => { + const changedConstants = (constants || []).filter((constant) => { + const nextValue = values[constant.key] + return JSON.stringify(nextValue) !== JSON.stringify(constant.value) + }).map((constant) => ({ + key: constant.key, + value: values[constant.key], + })) + + if (changedConstants.length === 0) { + window.alert('No constants were modified.') + return + } + + updateConstants( + { constants: changedConstants }, + { + onSuccess: (response) => { + window.alert(`Updated ${response.updatedCount} constants successfully.`) + refetch() + }, + onError: (error) => { + window.alert('Failed to update constants. Please try again.') + console.error('Update constants error:', error) + }, + } + ) + } + + if (isLoadingConstants) { + return ( +
+ Loading constants... +
+ ) + } + + if (!constants) { + return ( +
+ Failed to load constants +
+ ) + } + + const initialValues: ConstantFormData = constants.reduce((acc, constant) => { + acc[constant.key] = constant.value ?? '' + return acc + }, {} as ConstantFormData) + + return ( + +
+ Customize App Constants + + Modify application constants. Changes will take effect immediately. + + + + {({ handleSubmit, values, setFieldValue }) => ( +
+ {constants.map((constant) => { + if (!CONST_VISIBILITY[constant.key]) { + return null + } + + return ( +
+ +
+ ) + })} + + +
+ )} +
+
+
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.index.tsx b/apps/admin-web/src/routes/dashboard.index.tsx new file mode 100644 index 0000000..a5c1535 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.index.tsx @@ -0,0 +1,305 @@ +import React from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { + ShoppingBag, + Route as RouteIcon, + CalendarClock, + CirclePlus, + TriangleAlert, + Package, + DollarSign, + Tag, + Shapes, + Store, + TicketPercent, + Settings, + Users, + Megaphone, + ReceiptText, + ChevronRight, +} from 'lucide-react' +import { p as MyText } from 'web-components' +import { trpc } from '../lib/trpc-client' +import { useNavigationTarget } from '../lib/navigation-target' +import { theme } from '../lib/theme' + +export const Route = createFileRoute('/dashboard/')({ + component: Dashboard, +}) + +interface MenuItem { + title: string + icon: React.ReactNode + description?: string + route: string + category: 'quick' | 'products' | 'orders' | 'marketing' | 'settings' | 'users' + iconColor?: string + iconBg?: string + badgeCount?: number + onPress?: () => void + testID?: string +} + +const MenuItemComponent: React.FC<{ item: MenuItem; go: (item: MenuItem) => void }> = ({ + item, + go, +}) => { + return ( + + ) +} + +function Dashboard() { + const navigate = useNavigate() + const { setNavigationTarget } = useNavigationTarget() + + const { data: essentialsData } = trpc.admin.user.getEssentials.useQuery() + + const go = (item: MenuItem) => { + if (item.onPress) { + item.onPress() + } else { + navigate({ to: item.route as any }) + } + } + + const handleManageOrdersPress = () => { + setNavigationTarget('/dashboard/orders/list') + navigate({ to: '/dashboard/orders' }) + } + + const handleDeliverySequencesPress = () => { + setNavigationTarget('/dashboard/orders/sequence') + navigate({ to: '/dashboard/orders' }) + } + + const menuItems: MenuItem[] = [ + { + title: 'Manage Orders', + icon: , + description: 'View and manage customer orders', + route: '/dashboard/orders', + category: 'orders', + iconColor: '#10B981', + iconBg: '#D1FAE5', + onPress: handleManageOrdersPress, + }, + { + title: 'Delivery Sequences', + icon: , + description: 'Plan and optimize delivery routes', + route: '/dashboard/orders/sequence', + category: 'orders', + iconColor: '#8B5CF6', + iconBg: '#EDE9FE', + onPress: handleDeliverySequencesPress, + }, + { + title: 'Delivery Slots', + icon: , + description: 'Configure delivery times', + route: '/dashboard/slots', + category: 'quick', + iconColor: '#06B6D4', + iconBg: '#CFFAFE', + testID: 'delivery-slots-menu-item', + }, + { + title: 'Add Product', + icon: , + description: 'Create a new product listing', + route: '/dashboard/products/new', + category: 'quick', + iconColor: theme.colors.brand500, + iconBg: theme.colors.brand50, + }, + { + title: 'Complaints', + icon: , + description: 'Handle customer complaints', + route: '/dashboard/complaints', + category: 'quick', + iconColor: '#F59E0B', + iconBg: '#FEF3C7', + badgeCount: essentialsData?.unresolvedComplaints, + }, + { + title: 'Products', + icon: , + description: 'Manage product catalog', + route: '/dashboard/products', + category: 'products', + iconColor: '#6366F1', + iconBg: '#E0E7FF', + }, + { + title: 'Prices Overview', + icon: , + description: 'Update product pricing', + route: '/dashboard/prices', + category: 'products', + iconColor: '#059669', + iconBg: '#ECFDF5', + }, + { + title: 'Product Tags', + icon: , + description: 'Organize with tags', + route: '/dashboard/product-tags', + category: 'products', + iconColor: '#EC4899', + iconBg: '#FCE7F3', + }, + { + title: 'Product Groupings', + icon: , + description: 'Group products together', + route: '/dashboard/product-groupings', + category: 'products', + iconColor: '#8B5CF6', + iconBg: '#F3E8FF', + }, + { + title: 'Stores', + icon: , + description: 'Manage store locations', + route: '/dashboard/stores', + category: 'settings', + iconColor: '#14B8A6', + iconBg: '#CCFBF1', + }, + { + title: 'Coupons', + icon: , + description: 'Create discount coupons', + route: '/dashboard/coupons', + category: 'marketing', + iconColor: '#F97316', + iconBg: '#FFEDD5', + }, + { + title: 'App Constants', + icon: , + description: 'Customize app settings', + route: '/dashboard/customize-app', + category: 'settings', + iconColor: '#7C3AED', + iconBg: '#F3E8FF', + }, + { + title: 'User Management', + icon: , + description: 'View and manage all users', + route: '/dashboard/users', + category: 'users', + iconColor: '#0EA5E9', + iconBg: '#E0F2FE', + }, + { + title: 'Send Notifications', + icon: , + description: 'Send push notifications to users', + route: '/dashboard/notifications', + category: 'users', + iconColor: '#8B5CF6', + iconBg: '#F3E8FF', + }, + ] + + const quickActions = menuItems.filter((item) => item.category === 'quick') + + const categories = [ + { key: 'orders', title: 'Orders', icon: }, + { + key: 'products', + title: 'Products & Inventory', + icon: , + }, + { + key: 'marketing', + title: 'Marketing & Promotions', + icon: , + }, + { key: 'users', title: 'User Management', icon: }, + { + key: 'settings', + title: 'Settings & Configuration', + icon: , + }, + ] + + return ( +
+ {/* Quick Actions */} +
+ Quick Actions +
+ {quickActions.map((item) => ( + + ))} +
+
+ + {/* Categorized Menu Items */} +
+ {categories.map((category) => { + const categoryItems = menuItems.filter((item) => item.category === category.key) + if (categoryItems.length === 0) return null + + return ( +
+
+ + {category.icon} + + {category.title} +
+ {categoryItems.map((item) => ( + + ))} +
+ ) + })} +
+
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.notifications.tsx b/apps/admin-web/src/routes/dashboard.notifications.tsx new file mode 100644 index 0000000..4193039 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.notifications.tsx @@ -0,0 +1,168 @@ +import React, { useState } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { ChevronLeft } from 'lucide-react' +import { AppContainer, p as MyText, pInput as MyTextInput } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import type { UserMiniInfo } from '@packages/shared' +import { MultiSelect } from '../components/MultiSelect' + +export const Route = createFileRoute('/dashboard/notifications')({ + component: SendNotifications, +}) + +type User = Omit & { + name: string | null + isEligibleForNotif: boolean +} + +function SendNotifications() { + const [selectedUserIds, setSelectedUserIds] = useState([]) + const [title, setTitle] = useState('') + const [message, setMessage] = useState('') + const [searchQuery, setSearchQuery] = useState('') + + const { data: usersData, isLoading: isLoadingUsers } = trpc.admin.user.getUsersForNotification.useQuery({ + search: searchQuery, + }) + + const sendNotification = trpc.admin.user.sendNotification.useMutation({ + onSuccess: () => { + window.alert('Success: Notification sent successfully!') + setSelectedUserIds([]) + setTitle('') + setMessage('') + }, + onError: (error: any) => { + window.alert(`Error: ${error.message || 'Failed to send notification'}`) + }, + }) + + const eligibleUsers = usersData?.users.filter((u: User) => u.isEligibleForNotif) || [] + + const dropdownOptions = eligibleUsers.map((user: User) => ({ + label: `${user.mobile || 'No Mobile'}${user.name ? ` - ${user.name}` : ''}`, + value: user.id, + })) + + const handleSend = async () => { + if (title.trim().length === 0) { + window.alert('Error: Please enter a title') + return + } + + if (message.trim().length === 0) { + window.alert('Error: Please enter a message') + return + } + + const isSendingToAll = selectedUserIds.length === 0 + if (isSendingToAll) { + const confirmed = window.confirm( + 'Send to All Users? This will send the notification to all users with push tokens. Continue?' + ) + if (!confirmed) return + } + + try { + await sendNotification.mutateAsync({ + userIds: selectedUserIds, + title: title.trim(), + text: message.trim(), + }) + } catch (error: any) { + window.alert(`Error: ${error.message || 'Failed to send notification'}`) + } + } + + const getDisplayText = () => { + if (selectedUserIds.length === 0) return 'All Users' + if (selectedUserIds.length === 1) { + const user = eligibleUsers.find((u: User) => u.id === selectedUserIds[0]) + return user ? `${user.mobile}${user.name ? ` - ${user.name}` : ''}` : '1 user selected' + } + return `${selectedUserIds.length} users selected` + } + + if (isLoadingUsers) { + return ( + +
+
+ Loading users... +
+ + ) + } + + const isSendDisabled = sendNotification.isPending || title.trim().length === 0 || message.trim().length === 0 + + return ( + +
+
+ + Send Notifications +
+ +
+
+ Title + setTitle(e.target.value)} + placeholder="Enter notification title..." + className="text-gray-900" + /> +
+ +
+ Message + setMessage(e.target.value)} + placeholder="Enter notification message..." + multiline + numberOfLines={4} + className="text-gray-900" + /> +
+ +
+ Select Users (Optional) + setSelectedUserIds(value as number[])} + onSearch={(query: string) => setSearchQuery(query)} + /> + + {getDisplayText()} + + + Leave empty to send to all users + +
+ + +
+
+
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.orders.$id.tsx b/apps/admin-web/src/routes/dashboard.orders.$id.tsx new file mode 100644 index 0000000..2f02e01 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.orders.$id.tsx @@ -0,0 +1,824 @@ +import React, { useState } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { AppContainer, p as MyText, BottomDialog, pInput as MyInput } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' +import { theme } from '@/lib/theme' +import { + Clock, + Zap, + CircleAlert, + Check, + Package, + Truck, + ChevronRight, + User, + Phone, + Mail, + MapPin, + CircleX, + TicketPercent, + StickyNote, + Info, + Percent, + Banknote +} from 'lucide-react' +import dayjs from 'dayjs' +import CancelOrderDialog from '../components/CancelOrderDialog' +import { UserIncidentsView } from '../components/UserIncidentsView' + +export const Route = createFileRoute('/dashboard/orders/$id')({ + component: OrderDetails, +}) + +function OrderDetails() { + const { id } = Route.useParams() + const navigate = useNavigate() + const [generateCouponDialogOpen, setGenerateCouponDialogOpen] = + useState(false) + const [initiateRefundDialogOpen, setInitiateRefundDialogOpen] = + useState(false) + const [cancelDialogOpen, setCancelDialogOpen] = useState(false) + const [refundType, setRefundType] = useState<'percent' | 'amount'>('percent') + const [refundValue, setRefundValue] = useState('100') + const [updatingItems, setUpdatingItems] = useState>(new Set()) + + const { + data: orderData, + isLoading, + error, + refetch, + } = trpc.admin.order.getOrderDetails.useQuery( + { orderId: id ? parseInt(id) : 0 }, + { enabled: !!id } + ) + + const generateCouponMutation = + trpc.admin.coupon.generateCancellationCoupon.useMutation({ + onSuccess: (coupon) => { + window.alert( + `Success: Refund coupon generated successfully!\n\nCode: ${coupon.couponCode + }\nValue: ₹${coupon.flatDiscount}\nExpires: ${coupon.validTill + ? new Date(coupon.validTill).toLocaleDateString() + : 'N/A' + }` + ) + setGenerateCouponDialogOpen(false) + }, + onError: (error: any) => { + ErrorToast( + error.message || 'Failed to generate refund coupon' + ) + }, + }) + + const initiateRefundMutation = trpc.admin.payments.initiateRefund.useMutation( + { + onSuccess: () => { + window.alert( + 'Success: Refund initiated successfully!\n\nAmount: ' + // `Refund initiated successfully!\n\nAmount: ₹${result.amount}\nStatus: ${result.status}` + ) + setInitiateRefundDialogOpen(false) + }, + onError: (error: any) => { + ErrorToast(error.message || 'Failed to initiate refund') + }, + } + ) + + const updateItemPackagingMutation = trpc.admin.order.updateOrderItemPackaging.useMutation({ + onSuccess: () => { + // Refetch order details to get updated packaging status + refetch() + }, + onError: (error: any) => { + ErrorToast(error.message || 'Failed to update packaging status') + }, + }) + + const removeDeliveryChargeMutation = trpc.admin.order.removeDeliveryCharge.useMutation({ + onSuccess: () => { + window.alert('Success: Delivery charge has been removed') + refetch() + }, + onError: (error: any) => { + ErrorToast(error.message || 'Failed to remove delivery charge') + }, + }) + + if (isLoading) { + return ( +
+ + Loading order details... + +
+ ) + } + + if (error || !orderData) { + return ( +
+
+ + + Oops! + + + {error?.message || 'Failed to load order details'} + + +
+
+ ) + } + + const order = orderData + + // Calculate subtotal and discount for order summary + const subtotal = order.items.reduce((sum, item) => sum + item.amount, 0) + const discountAmount = order.discountAmount || 0 + + const handleGenerateCoupon = () => { + generateCouponMutation.mutate({ orderId: order.id }) + } + + const handleInitiateRefund = () => { + const value = parseFloat(refundValue) + if (isNaN(value) || value <= 0) { + ErrorToast('Please enter a valid refund value') + return + } + + const mutationData: any = { + orderId: order.id, + } + + if (refundType === 'percent') { + if (value > 100) { + ErrorToast('Refund percentage cannot exceed 100%') + return + } + mutationData.refundPercent = value + } else { + mutationData.refundAmount = value + } + + initiateRefundMutation.mutate(mutationData) + setInitiateRefundDialogOpen(false) + } + + const handlePackagingToggle = (itemId: number, field: 'isPackaged' | 'isPackageVerified', value: boolean) => { + // Add item to updating set to disable UI + setUpdatingItems((prev) => new Set(prev).add(itemId)) + + updateItemPackagingMutation.mutate( + { + orderItemId: itemId, + [field]: value, + }, + { + onSettled: () => { + // Remove item from updating set + setUpdatingItems((prev) => { + const newSet = new Set(prev) + newSet.delete(itemId) + return newSet + }) + }, + } + ) + } + + const getStatusColor = (status: string) => { + switch (status) { + case 'delivered': + return 'text-green-600 bg-green-50 border-green-100' + case 'cancelled': + return 'text-red-600 bg-red-50 border-red-100' + default: + return 'text-yellow-600 bg-yellow-50 border-yellow-100' + } + } + + const statusStyle = getStatusColor(order.status) + + return ( + + +
+ {/* Order ID & Status Card */} +
+
+
+ Order ID + + #{order.readableId} + +
+
+ + + {order.status} + + + {order.isFlashDelivery && ( + + + + )} +
+
+ +
+ + + {order.isFlashDelivery ? '1 Hr Delivery:' : 'Delivery at:'} {order.isFlashDelivery + ? dayjs(order.createdAt).add(30, 'minutes').format('MMM DD, YYYY • h:mm A') + : order.slotInfo?.time ? dayjs(order.slotInfo.time).format('MMM DD, YYYY • h:mm A') : 'Not scheduled'} + +
+ {order.isFlashDelivery && ( +
+ + + ⚡ 1 Hr Delivery • High Priority + +
+ )} +
+ + + Placed on {dayjs(order.createdAt).format('MMM DD, YYYY • h:mm A')} + +
+
+ + {/* Cancellation Reason */} + {order.status === 'cancelled' && order.cancelReason && ( +
+
+ + + Cancellation Reason + +
+ + {order.cancelReason} + +
+ )} + + {/* Order Progress (Simplified Timeline) */} + {order.status !== 'cancelled' && ( +
+ + Order Status + +
+ {/* Placed */} +
+ + + + + Placed + +
+
+ + {/* Packaged */} +
+ + + + + Packaged + +
+
+ + {/* Delivered */} +
+ + {order.isFlashDelivery && order.isDelivered ? ( + + ) : ( + + )} + +
+ + {order.isFlashDelivery && order.isDelivered ? 'Flash Delivered' : 'Delivered'} + + {order.isFlashDelivery && ( + + ⚡ 30-Min + + )} +
+
+
+
+ )} + + {/* Customer Details */} + + + {/* Order Items */} +
+ + Items Ordered + + {order.items.map((item, index) => ( +
+ + + +
+ + {item.name} + + + {Number(item.quantity)} x {item.unit} × ₹{item.price} + +
+ + +
+
+ + ₹{item.amount} + +
+ ))} + +
+
+ + Subtotal ({order.items.length} items) + + + ₹{subtotal} + +
+ {discountAmount > 0 && ( +
+ + Discount + + + -₹{discountAmount} + +
+ )} + {order.deliveryCharge > 0 && ( +
+
+ + Delivery Charge + + +
+ + ₹{order.deliveryCharge} + +
+ )} +
+
+ + Total Amount + + {order.isFlashDelivery && ( + + ⚡ FLASH + + )} +
+ + ₹{order.totalAmount} + +
+
+
+ + {/* 1 Hr Delivery Priority Notice */} + {order.isFlashDelivery && ( +
+
+ + + 1 Hr Delivery Order + +
+ + ⚡ This is a high-priority flash delivery order that must be delivered within 1 hr of placement. + +
+ + Expected Delivery: {dayjs(order.createdAt).add(30, 'minutes').format('MMM DD, YYYY • h:mm A')} + +
+
+ )} + + {/* Admin Notes */} + {order.adminNotes && ( +
+
+ + + Admin Notes + +
+ + {order.adminNotes} + +
+ )} + + {/* User Incidents Section */} + {order.userId && ( + + )} + + {/* Coupon Applied Section */} + {order.couponCode && ( +
+ + Coupon Applied + +
+
+ + + {order.couponCode} + +
+ + {order.couponDescription} + +
+ + Discount Applied: + + + -₹{order.discountAmount} + +
+
+
+ )} + + {/* Refund Coupon Section */} + {order.orderStatus?.refundCouponId && ( +
+ + Refund Coupon + +
+
+ + + {order.couponCode} + +
+ + Generated refund coupon for order cancellation + +
+ + Value: + + + ₹{order.couponData?.discountAmount} + +
+
+
+ )} + + {/* TEMPORARILY HIDDEN: Refund Details Section */} + {/* WARNING: This section contains functional refund and cancellation management features */} + {/* DO NOT REMOVE - This is temporarily commented out for UI simplification */} + {/* When ready to re-enable, simply uncomment the entire block below */} +
+
+ + {/* Bottom Action Bar */} +
+ {order.status !== 'cancelled' && ( + + )} + +
+ + {/* Generate Coupon Dialog */} + setGenerateCouponDialogOpen(false)} + > +
+
+ + + + + Generate Refund Coupon + + + Create a one-time use coupon for the customer equal to the order amount. Valid for 30 days. + +
+ +
+ + + This only works for online payment orders. COD orders cannot generate refund coupons. + +
+ +
+ + +
+
+
+ + {/* Initiate Refund Dialog */} + setInitiateRefundDialogOpen(false)} + > +
+
+ + + + + Initiate Refund + + + Process a refund directly to the customer's source account via Razorpay. + +
+ + {/* Refund Type Selection */} +
+ + Refund Type + +
+ + + +
+
+ + {/* Refund Value Input */} +
+ setRefundValue(e.target.value)} + type="number" + placeholder={refundType === 'percent' ? '100' : '0.00'} + className="bg-white" + /> +
+ +
+ + + For COD orders, refunds are processed immediately upon delivery confirmation. + +
+ +
+ + +
+
+
+ + {/* Cancel Order Dialog */} + setCancelDialogOpen(false)} + onSuccess={refetch} + /> + + ) +} diff --git a/apps/admin-web/src/routes/dashboard.orders.list.tsx b/apps/admin-web/src/routes/dashboard.orders.list.tsx new file mode 100644 index 0000000..0c1076b --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.orders.list.tsx @@ -0,0 +1,818 @@ +import React, { useState, useEffect } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { + p as MyText, + pInput as MyInput, + MyButton, + AppContainer, + BottomDialog, + Checkbox, + Dropdown, + SearchBar, + MyFlatList +} from 'web-components' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' +import dayjs from 'dayjs' +import { + Zap, + Clock, + TriangleAlert, + MapPin, + CalendarDays, + ShoppingCart, + StickyNote, + ShieldCheck, + EllipsisVertical, + ListFilter, + Loader2 +} from 'lucide-react' +import CancelOrderDialog from '../components/CancelOrderDialog' +import { OrderOptionsMenu } from '../components/OrderOptionsMenu' +import type { AdminOrderListItem, AdminOrderListItemProduct } from '@packages/shared' + +export const Route = createFileRoute('/dashboard/orders/list')({ + validateSearch: (s: Record) => ({ + filter: (s.filter as string) ?? '', + slotId: (s.slotId as string) ?? '' + }), + component: Orders, +}) + +const AdminNotesForm = ({ orderId, existingNotes, onClose, refetch }: { orderId: string, existingNotes?: string | null, onClose: () => void, refetch: () => void }) => { + const [notesText, setNotesText] = useState(existingNotes || '') + const updateNotesMutation = trpc.admin.order.updateNotes.useMutation() + + return ( +
+ Admin Notes + setNotesText(e.target.value)} + placeholder="Enter admin notes..." + className="min-h-24" + /> +
+ { + updateNotesMutation.mutate( + { orderId: parseInt(orderId), adminNotes: notesText }, + { + onSuccess: () => { + onClose() + window.alert('Success: Notes updated successfully') + refetch() + }, + onError: (error: any) => { + ErrorToast(error.message || 'Failed to update notes') + }, + } + ) + }} + > + Save + +
+
+ ) +} + +// Order list row — derived from shared AdminOrderListItem (serialized dates, +// coupon display fields, optional flags) +type OrderItemRow = Omit< + AdminOrderListItemProduct, + 'id' | 'skuName' | 'features' | 'isPackaged' | 'isPackageVerified' +> & { + id?: number, + isPackaged?: boolean, + isPackageVerified?: boolean +} + +type OrderType = Omit< + AdminOrderListItem, + 'customerName' | 'customerMobile' | 'items' | 'createdAt' | 'adminNotes' | 'userNotes' | 'userNegativityScore' +> & { + customerName: string | null, + customerMobile?: string | null, + items: OrderItemRow[], + createdAt: string, + adminNotes?: string | null, + userNotes?: string | null, + userNegativityScore?: number, + couponCode?: string, + couponDescription?: string, + discountAmount?: number +} + +const OrderItem = ({ order, refetch }: { order: OrderType, refetch: () => void }) => { + const navigate = useNavigate() + const [menuOpen, setMenuOpen] = useState(false) + const [itemsDialogOpen, setItemsDialogOpen] = useState(false) + const [notesDialogOpen, setNotesDialogOpen] = useState(false) + const [cancelDialogOpen, setCancelDialogOpen] = useState(false) + const [userNotesDialogOpen, setUserNotesDialogOpen] = useState(false) + const [adminNotesDialogOpen, setAdminNotesDialogOpen] = useState(false) + const [updatingItems, setUpdatingItems] = useState>(new Set()) + + const updatePackagedMutation = trpc.admin.order.updatePackaged.useMutation() + const updateDeliveredMutation = trpc.admin.order.updateDelivered.useMutation() + const updateItemPackagingMutation = trpc.admin.order.updateOrderItemPackaging.useMutation() + + const handleOrderPress = () => { + navigate({ to: '/dashboard/orders/$id', params: { id: String(order.orderId) } }) + } + + const handleMenuOption = () => { + setMenuOpen(false) + navigate({ to: '/dashboard/orders/$id', params: { id: String(order.orderId) } }) + } + + const handleMarkPackaged = (isPackaged: boolean) => { + updatePackagedMutation.mutate( + { orderId: order.orderId.toString(), isPackaged }, + { + onSuccess: () => { + refetch() + }, + } + ) + } + + const handleMarkDelivered = (isDelivered: boolean) => { + updateDeliveredMutation.mutate( + { orderId: order.orderId.toString(), isDelivered }, + { + onSuccess: () => { + refetch() + }, + } + ) + } + + const handleItemPackagingToggle = (itemId: number, field: 'isPackaged' | 'isPackageVerified', value: boolean) => { + setUpdatingItems((prev) => new Set(prev).add(itemId)) + + updateItemPackagingMutation.mutate( + { orderItemId: itemId, [field]: value }, + { + onSuccess: () => { + setUpdatingItems((prev) => { + const newSet = new Set(prev) + newSet.delete(itemId) + return newSet + }) + refetch() + }, + onError: (error: any) => { + setUpdatingItems((prev) => { + const newSet = new Set(prev) + newSet.delete(itemId) + return newSet + }) + ErrorToast(error.message || 'Failed to update packaging status') + }, + } + ) + } + + return ( + <> +
{ if (e.key === 'Enter') handleOrderPress() }} + className="mx-4 mb-4 flex w-[calc(100%-2rem)] cursor-pointer flex-col overflow-hidden rounded-xl border border-gray-100 bg-white text-left shadow-sm" + > + {/* Header Section */} +
+
+
+
+ 0 ? 'text-yellow-600' : 'text-gray-900')}`}> + {order.customerName || order.customerMobile || 'Unknown Customer'} + + + #{order.readableId} + + {order.isFlashDelivery && ( + + + FLASH + + )} +
+
+ + + {dayjs(order.createdAt).format('MMM D, h:mm A')} + + {order.userNegativityScore && order.userNegativityScore > 0 && ( + + + Negative Customer + + )} +
+
+ + +
+
+ + {/* Main Content */} +
+ {/* Status Badges */} +
+ + Packaged + {}} + size={18} + fillColor="#6B7280" + checkColor="#FFFFFF" + /> + + e.stopPropagation()} + > + Delivered + handleMarkDelivered(!order.isDelivered)} + size={18} + fillColor="#10B981" + checkColor="#FFFFFF" + /> + + {order.status === 'cancelled' && ( + + CANCELLED + + )} +
+ + {/* Delivery Info */} +
+ +
+ Delivery Address + + {order.address} + +
+ + + {order.isFlashDelivery ? '1 Hr Delivery:' : 'Slot:'} {order.isFlashDelivery ? dayjs(order.createdAt).add(30, 'minutes').format('MMM D, h:mm A') : order.deliveryTime ? dayjs(order.deliveryTime).format('ddd, MMM D • h:mm A') : 'Not scheduled'} + +
+ {order.isFlashDelivery && ( +
+ + + 1 Hour Delivery • High Priority + +
+ )} +
+
+ + {/* Items Summary & Total */} +
+
+
+ + + Total: + {`₹${order.totalAmount}`} + +
+
+
+ + {/* Coupons */} + {order.couponCode && ( +
+ Applied Coupons +
+ + {order.couponCode} + + {order.couponDescription && ( + + {order.couponDescription} + + )} + {order.discountAmount && ( + + {`Discount: ₹${order.discountAmount}`} + + )} +
+
+ )} + + {/* Notes Section */} +
+ {order.userNotes && ( + + )} + {order.adminNotes && ( + + )} +
+ + {/* Footer / Delivery Charge */} + {order.deliveryCharge > 0 && ( +
+
+ Delivery Charge + {`₹${order.deliveryCharge}`} +
+
+ )} +
+
+ + setMenuOpen(false)} + order={{ + id: order.id, + readableId: order.readableId, + isPackaged: order.isPackaged, + isDelivered: order.isDelivered, + isFlashDelivery: order.isFlashDelivery, + address: order.address, + addressId: order.addressId, + adminNotes: order.adminNotes, + userNotes: order.userNotes, + latitude: order.latitude, + longitude: order.longitude, + status: order.status, + }} + onViewDetails={handleMenuOption} + onTogglePackaged={() => handleMarkPackaged(!order.isPackaged)} + onToggleDelivered={() => handleMarkDelivered(!order.isDelivered)} + onOpenAdminNotes={() => { + setMenuOpen(false) + setNotesDialogOpen(true) + }} + onCancelOrder={() => { + setMenuOpen(false) + setCancelDialogOpen(true) + }} + onAttachLocation={() => refetch()} + onWhatsApp={() => {}} + onDial={() => {}} + /> + + setItemsDialogOpen(false)}> +
+
+ + Order Items + + {order.isFlashDelivery && ( + + + FLASH + + )} +
+ + {`Total: ₹${order.totalAmount}`} + + {order.items.map((item, idx) => ( +
+
+ + {`${item.quantity} x ${item.unit}`} + + + {item.name.length > 30 ? `${item.name.substring(0, 30)}...` : item.name} + + {item.isPackaged !== undefined && item.isPackageVerified !== undefined && ( + <> + + pkg + handleItemPackagingToggle(item.id!, 'isPackaged', !item.isPackaged)} + size={18} + fillColor={updatingItems.has(item.id!) ? '#F59E0B' : '#10B981'} + checkColor="#FFFFFF" + /> + + + verf + handleItemPackagingToggle(item.id!, 'isPackageVerified', !item.isPackageVerified)} + size={18} + fillColor={updatingItems.has(item.id!) ? '#F59E0B' : '#10B981'} + checkColor="#FFFFFF" + /> + + {updatingItems.has(item.id!) && ( + + )} + + )} +
+
+ ))} +
+
+ + setNotesDialogOpen(false)}> + setNotesDialogOpen(false)} refetch={refetch} /> + + + setCancelDialogOpen(false)} + onSuccess={refetch} + /> + + setUserNotesDialogOpen(false)}> +
+ + User Notes + +
+ + {order.userNotes} + +
+
+
+ + setAdminNotesDialogOpen(false)}> +
+ + Admin Notes + +
+ + {order.adminNotes} + +
+
+
+ + ) +} + +function Orders() { + const { filter, slotId } = Route.useSearch() + const [selectedSlot, setSelectedSlot] = useState(null) + const [selectedSlotType, setSelectedSlotType] = useState<'slot' | 'flash' | null>(null) + const [packagedFilter, setPackagedFilter] = useState<'all' | 'packaged' | 'not_packaged'>('all') + const [packagedChecked, setPackagedChecked] = useState(false) + const [notPackagedChecked, setNotPackagedChecked] = useState(false) + const [deliveredFilter, setDeliveredFilter] = useState<'all' | 'delivered' | 'not_delivered'>('all') + const [deliveredChecked, setDeliveredChecked] = useState(false) + const [notDeliveredChecked, setNotDeliveredChecked] = useState(false) + const [cancellationFilter, setCancellationFilter] = useState<'all' | 'cancelled' | 'not_cancelled'>('all') + const [cancelledChecked, setCancelledChecked] = useState(false) + const [notCancelledChecked, setNotCancelledChecked] = useState(false) + const [flashDeliveryFilter, setFlashDeliveryFilter] = useState<'all' | 'flash' | 'regular'>('all') + const [flashChecked, setFlashChecked] = useState(false) + const [regularChecked, setRegularChecked] = useState(false) + const [filterDialogOpen, setFilterDialogOpen] = useState(false) + + // Handle initial filter from URL params + useEffect(() => { + if (filter === 'flash' || slotId === 'flash') { + setSelectedSlotType('flash') + setFlashDeliveryFilter('flash') + setFlashChecked(true) + setRegularChecked(false) + } else if (slotId && !Number.isNaN(Number(slotId))) { + setSelectedSlotType('slot') + setSelectedSlot(Number(slotId)) + } + }, [filter, slotId]) + const { data: slotsData } = trpc.admin.slots.getAll.useQuery() + const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage, refetch } = trpc.admin.order.getAll.useInfiniteQuery( + { + limit: 20, + slotId: selectedSlotType === 'slot' ? selectedSlot : null, + packagedFilter, + deliveredFilter, + cancellationFilter, + flashDeliveryFilter: selectedSlotType === 'flash' ? 'flash' : flashDeliveryFilter + }, + { + getNextPageParam: (lastPage) => lastPage?.nextCursor, + } + ) + + const orders = data?.pages.flatMap((page) => page?.orders) || [] + + if (isLoading) { + return ( +
+
+ Loading orders... +
+ ) + } + + const slotOptions = [ + { label: '⚡ Flash Deliveries', value: 'flash' }, + ...(slotsData?.slots?.map((slot) => ({ + label: dayjs(slot.deliveryTime).format('ddd DD MMM, h:mm a'), + value: slot.id.toString(), + })) || []) + ] + + return ( + <> + String(item?.orderId ?? index)} + renderItem={(item) => item ? : null} + onRefresh={() => refetch()} + ListHeaderComponent={ + <> +
+
+ { + if (val === 'flash') { + setSelectedSlotType('flash') + setSelectedSlot(null) + setFlashDeliveryFilter('flash') + // Reset other filters when switching to flash + setPackagedFilter('all') + setPackagedChecked(false) + setNotPackagedChecked(false) + setDeliveredFilter('all') + setDeliveredChecked(false) + setNotDeliveredChecked(false) + setCancellationFilter('all') + setCancelledChecked(false) + setNotCancelledChecked(false) + } else { + setSelectedSlotType('slot') + setSelectedSlot(val ? Number(val) : null) + setFlashDeliveryFilter('all') + } + }} + /> +
+ +
+ {!isLoading && selectedSlotType && ( +
+ + {selectedSlotType === 'flash' + ? `${orders.length} Flash delivery orders` + : `${orders.length} Orders in slot` + } + +
+ )} + + } + ListFooterComponent={ +
+ {isFetchingNextPage ? ( +
+
+ Loading more... +
+ ) : hasNextPage ? ( + fetchNextPage()}> + Load more + + ) : null} +
+ } + /> + + setFilterDialogOpen(false)}> + +
+ Packaged Status +
+ { + const newValue = !packagedChecked + setPackagedChecked(newValue) + if (newValue && notPackagedChecked) { + setPackagedFilter('all') + } else if (newValue) { + setPackagedFilter('packaged') + } else if (notPackagedChecked) { + setPackagedFilter('not_packaged') + } else { + setPackagedFilter('all') + } + }} + /> + Packaged +
+
+ { + const newValue = !notPackagedChecked + setNotPackagedChecked(newValue) + if (packagedChecked && newValue) { + setPackagedFilter('all') + } else if (newValue) { + setPackagedFilter('not_packaged') + } else if (packagedChecked) { + setPackagedFilter('packaged') + } else { + setPackagedFilter('all') + } + }} + /> + Not Packaged +
+
+
+ Delivered Status +
+ { + const newValue = !deliveredChecked + setDeliveredChecked(newValue) + if (newValue && notDeliveredChecked) { + setDeliveredFilter('all') + } else if (newValue) { + setDeliveredFilter('delivered') + } else if (notDeliveredChecked) { + setDeliveredFilter('not_delivered') + } else { + setDeliveredFilter('all') + } + }} + /> + Delivered +
+
+ { + const newValue = !notDeliveredChecked + setNotDeliveredChecked(newValue) + if (deliveredChecked && newValue) { + setDeliveredFilter('all') + } else if (newValue) { + setDeliveredFilter('not_delivered') + } else if (deliveredChecked) { + setDeliveredFilter('delivered') + } else { + setDeliveredFilter('all') + } + }} + /> + Not Delivered +
+
+
+ Cancellation Status +
+ { + const newValue = !cancelledChecked + setCancelledChecked(newValue) + if (newValue && notCancelledChecked) { + setCancellationFilter('all') + } else if (newValue) { + setCancellationFilter('cancelled') + } else if (notCancelledChecked) { + setCancellationFilter('not_cancelled') + } else { + setCancellationFilter('all') + } + }} + /> + Cancelled +
+
+ { + const newValue = !notCancelledChecked + setNotCancelledChecked(newValue) + if (cancelledChecked && newValue) { + setCancellationFilter('all') + } else if (newValue) { + setCancellationFilter('not_cancelled') + } else if (cancelledChecked) { + setCancellationFilter('cancelled') + } else { + setCancellationFilter('all') + } + }} + /> + Not Cancelled +
+
+
+ Delivery Type +
+ { + const newValue = !flashChecked + setFlashChecked(newValue) + if (newValue && regularChecked) { + setFlashDeliveryFilter('all') + } else if (newValue) { + setFlashDeliveryFilter('flash') + } else if (regularChecked) { + setFlashDeliveryFilter('regular') + } else { + setFlashDeliveryFilter('all') + } + }} + /> + ⚡ 1 Hr Delivery +
+
+ { + const newValue = !regularChecked + setRegularChecked(newValue) + if (flashChecked && newValue) { + setFlashDeliveryFilter('all') + } else if (newValue) { + setFlashDeliveryFilter('regular') + } else if (flashChecked) { + setFlashDeliveryFilter('flash') + } else { + setFlashDeliveryFilter('all') + } + }} + /> + Regular Delivery +
+
+
+
+ + ) +} diff --git a/apps/admin-web/src/routes/dashboard.orders.sequence.tsx b/apps/admin-web/src/routes/dashboard.orders.sequence.tsx new file mode 100644 index 0000000..5f7c378 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.orders.sequence.tsx @@ -0,0 +1,798 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { + DndContext, + closestCenter, + PointerSensor, + useSensor, + useSensors +} from '@dnd-kit/core' +import type { DragEndEvent } from '@dnd-kit/core' +import { + SortableContext, + useSortable, + verticalListSortingStrategy, + arrayMove +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { + p as MyText, + BottomDialog, + Checkbox, + Dropdown +} from 'web-components' +import dayjs from 'dayjs' +import { trpc } from '@/lib/trpc-client' +import { ErrorToast } from '@/services/toaster' +import { useManualRefresh, useMarkDataFetchers } from '@/lib/refresh-context' +import { + MapPin, + Map as MapIcon, + EllipsisVertical, + GripVertical, + Save, + ChevronLeft, + CalendarX, + ClipboardList +} from 'lucide-react' +import { OrderOptionsMenu } from '../components/OrderOptionsMenu' +import CancelOrderDialog from '../components/CancelOrderDialog' +import { OrderNotesForm } from '../components/OrderNotesForm' +import { SearchableSelect } from '@/components/SearchableSelect' + +export const Route = createFileRoute('/dashboard/orders/sequence')({ + validateSearch: (s: Record) => ({ + slotId: (s.slotId as string) ?? '' + }), + component: DeliverySequences, +}) + +// Define types outside +interface OrderWithSequence { + sequenceId: number, + readableId: number, + isPackaged: boolean, + totalAmount: number, + address: string, + latitude: number | null, + longitude: number | null, + id: number, + isDelivered: boolean, + addressId: number, + adminNotes?: string | null, + customerName?: string | null, + assignedUserName?: string | null +} + +interface SlotProgressProps { + slotId: number, + orders: any[] +} + +const SlotProgress: React.FC = ({ slotId, orders }) => { + const delivered = orders.filter((o) => o.isDelivered).length + const undelivered = orders.filter((o) => !o.isDelivered).length + const packaged = orders.filter((o) => o.isPackaged).length + const notPackaged = orders.filter((o) => !o.isPackaged).length + + const stats = [ + { label: 'Delivered', value: delivered }, + { label: 'Undelivered', value: undelivered }, + { label: 'Packaged', value: packaged }, + { label: 'Not Packaged', value: notPackaged }, + ] + + return ( +
+ + Slot {slotId} Statistics + +
+ {stats.map((stat, index) => ( +
+
+ + {stat.value} + + {stat.label} +
+
+ ))} +
+
+ ) +} + +interface OrderItemProps { + item: OrderWithSequence, + isPending: boolean, + setSelectedOrder: (order: OrderWithSequence | null) => void, + setShowOrderMenu: (show: boolean) => void, + selectedOrderIds: number[], + onToggleOrder: (id: number) => void, + assignedUserName?: string | null, + staffData?: { staff?: Array<{ id: number, name: string }> }, + setSelectedUserId?: (id: number) => void +} + +const OrderItem: React.FC = ({ + item, + isPending, + setSelectedOrder, + setShowOrderMenu, + selectedOrderIds, + onToggleOrder, + assignedUserName, + staffData, + setSelectedUserId, +}) => { + const orderItem = item + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id: orderItem.sequenceId, disabled: isPending }) + + return ( +
+
+
+ {/* Checkbox and Drag Handle */} +
+ onToggleOrder(orderItem.id)} + size={20} + fillColor="#3b82f6" + checkColor="#FFFFFF" + /> +
+ +
+
+ + {/* Content */} +
+
+
+ + #{orderItem.readableId} + + + + Pkg + + {}} + size={16} + fillColor="#6B7280" + checkColor="#FFFFFF" + /> + +
+
+ + + ₹{orderItem.totalAmount} + + + +
+
+ +
+ + + {orderItem.address} + + {orderItem.latitude && orderItem.longitude && ( + + )} +
+ {assignedUserName && ( +
+ + Assigned to{' '} + + +
+ )} +
+
+ {/* Admin Notes */} + {orderItem.adminNotes && ( +
+ + {orderItem.adminNotes} + +
+ )} +
+
+ ) +} + +function DeliverySequences() { + const { slotId } = Route.useSearch() + const selectedSlotId = slotId ? Number(slotId) : null + const [localOrderedOrders, setLocalOrderedOrders] = useState< + OrderWithSequence[] + >([]) + const [showOrderMenu, setShowOrderMenu] = useState(false) + const [selectedOrder, setSelectedOrder] = useState( + null + ) + const [selectedUserId, setSelectedUserId] = useState(-1) + const [selectedOrderIds, setSelectedOrderIds] = useState([]) + const [hasSequenceChanged, setHasSequenceChanged] = useState(false) + const [cancelDialogOpen, setCancelDialogOpen] = useState(false) + const [notesDialogOpen, setNotesDialogOpen] = useState(false) + const navigate = useNavigate() + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { delay: 200, tolerance: 8 } }) + ) + + const { data: slotsData, refetch: refetchSlots } = + trpc.admin.slots.getAll.useQuery() + const { + data: ordersData, + isLoading: ordersLoading, + refetch: refetchOrders, + } = trpc.admin.order.getSlotOrders.useQuery( + { slotId: String(selectedSlotId) }, + { + enabled: !!selectedSlotId, + } + ) + const { data: sequenceData, refetch: refetchSequence } = + trpc.admin.slots.getDeliverySequence.useQuery( + { id: String(selectedSlotId) }, + { + enabled: !!selectedSlotId, + } + ) + + const { data: staffData } = trpc.admin.staffUser.getStaff.useQuery() + + // Auto-select first slot if no slotId provided + useEffect(() => { + if (!slotId && slotsData?.slots && slotsData.slots.length > 0) { + navigate({ to: '/dashboard/orders/sequence', search: { slotId: String(slotsData.slots[0].id) }, replace: true }) + } + }, [slotId, slotsData, navigate]) + + const updateSequenceMutation = + trpc.admin.slots.updateDeliverySequence.useMutation() + const updatePackagedMutation = trpc.admin.order.updatePackaged.useMutation() + const updateDeliveredMutation = + trpc.admin.order.updateDelivered.useMutation() + + // Manual refresh functionality + useManualRefresh(() => { + refetchSlots() + refetchOrders() + refetchSequence() + }) + + useMarkDataFetchers(() => { + refetchSlots() + refetchOrders() + refetchSequence() + }) + + const slots = slotsData?.slots || [] + const orders = ordersData?.data || [] + const deliverySequence = sequenceData?.deliverySequence || [] + + const slotOptions = + slots?.map((slot) => ({ + label: dayjs(slot.deliveryTime).format('ddd DD MMM, h:mm a'), + value: slot.id, + })) || [] + + const userOptions = [ + { label: 'Unassigned', value: -1 }, + { label: 'All Users', value: -2 }, + ...(staffData?.staff?.map((staff) => ({ + label: staff.name, + value: staff.id, + })) || []), + ] + + // Create ordered orders based on delivery sequence + const computedOrderedOrders = useMemo(() => { + if (orders.length > 0) { + const userSequence = + selectedUserId !== -1 + ? (deliverySequence as any)?.[String(selectedUserId)] || [] + : null + if (selectedUserId !== -1 && userSequence && userSequence.length > 0) { + // Sort orders according to user's sequence + const sequenceMap = new Map( + userSequence.map((id: number, index: number) => [id, index]) + ) + const ordered = orders + .filter((order) => userSequence.includes(order.id)) + .map((order) => ({ ...order, sequenceId: order.id })) + .sort((a, b) => { + const aIndex = sequenceMap.get(a.sequenceId) ?? Infinity + const bIndex = sequenceMap.get(b.sequenceId) ?? Infinity + return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0 + }) + return ordered + } else if (selectedUserId === -1) { + // Show unassigned orders (not in any user's sequence) + const assignedIds = new Set( + Object.values(deliverySequence as any).flat() + ) + const ordered = orders + .filter((order) => !assignedIds.has(order.id)) + .map((order) => ({ ...order, sequenceId: order.id })) + .sort((a, b) => + a.sequenceId < b.sequenceId + ? -1 + : a.sequenceId > b.sequenceId + ? 1 + : 0 + ) + return ordered + } else if (selectedUserId === -2) { + // Show all orders with assignment info + const orderToUserMap = new Map() + Object.entries(deliverySequence as any).forEach(([userId, orderIds]) => { + (orderIds as number[]).forEach((orderId) => { + orderToUserMap.set(orderId, parseInt(userId)) + }) + }) + const ordered = orders + .map((order) => { + const assignedUserId = orderToUserMap.get(order.id) + const assignedUser = staffData?.staff?.find( + (s) => s.id === assignedUserId + ) + return { + ...order, + sequenceId: order.id, + assignedUserName: assignedUser?.name || null, + } + }) + .sort((a, b) => + a.sequenceId < b.sequenceId + ? -1 + : a.sequenceId > b.sequenceId + ? 1 + : 0 + ) + return ordered + } else { + // No sequence for user, show empty + return [] + } + } else { + return [] + } + }, [ordersData, sequenceData, selectedUserId]) + + // Sync local state with computed orders + useEffect(() => { + setLocalOrderedOrders(computedOrderedOrders as OrderWithSequence[]) + }, [computedOrderedOrders]) + + const handleDragEnd = ({ data }: { data: OrderWithSequence[] }) => { + if (selectedUserId !== -1 && selectedUserId !== -2) { + setLocalOrderedOrders(data) + setHasSequenceChanged(true) + } + } + + const handleDndDragEnd = (event: DragEndEvent) => { + const { active, over } = event + if (!over || active.id === over.id) return + const oldIndex = localOrderedOrders.findIndex((o) => o.sequenceId === active.id) + const newIndex = localOrderedOrders.findIndex((o) => o.sequenceId === over.id) + if (oldIndex === -1 || newIndex === -1) return + handleDragEnd({ data: arrayMove(localOrderedOrders, oldIndex, newIndex) }) + } + + const handleSaveSequence = () => { + if (!selectedSlotId || selectedUserId === -1) return + + const newSequence = { ...(deliverySequence as any) } + newSequence[String(selectedUserId)] = localOrderedOrders.map( + (order) => order.sequenceId + ) + + updateSequenceMutation.mutate( + { + id: selectedSlotId, + deliverySequence: newSequence, + }, + { + onSuccess: () => { + setHasSequenceChanged(false) + window.alert('Success: Delivery sequence updated successfully') + }, + onError: (error: any) => { + ErrorToast( + `Failed to update delivery sequence: ${ + error.message || 'Unknown error' + }` + ) + }, + } + ) + } + + if (!slotsData) { + return ( +
+
+ Loading slots... +
+ ) + } + + if (slotsData.slots.length === 0) { + return ( +
+ + + No slots available + +
+ ) + } + + return ( +
+ {selectedSlotId ? ( + <> + {/* Header Section */} +
+ + +
+ { + if (val !== '' && val !== null && val !== undefined) { + navigate({ to: '/dashboard/orders/sequence', search: { slotId: String(Number(val)) }, replace: true }) + } + }} + /> +
+
+ setSelectedUserId(Number(val))} + /> +
+
+ + {/* Content Section */} + {ordersLoading ? ( +
+
+ Loading orders... +
+ ) : localOrderedOrders.length === 0 ? ( +
+ + + No orders found for this slot + +
+ ) : ( +
+ + + o.sequenceId)} + strategy={verticalListSortingStrategy} + > + {localOrderedOrders.map((item) => ( + { + setSelectedOrderIds((prev) => + prev.includes(id) + ? prev.filter((i) => i !== id) + : [...prev, id] + ) + }} + assignedUserName={selectedUserId === -2 ? item.assignedUserName : undefined} + staffData={staffData} + setSelectedUserId={setSelectedUserId} + /> + ))} + + +
+ )} + + {/* Assignment panel (replaces FAB-triggered dropdown; SearchableSelect renders its own trigger) */} + {selectedOrderIds.length > 0 && ( +
+ opt.value !== -1), + { label: 'None', value: -3 }, + ]} + value={-1} + onValueChange={(val) => { + const numVal = Number(val) + if (numVal !== -1) { + if (numVal === -3) { + // Unassign selected orders + const newSequence = { ...(deliverySequence as any) } + Object.keys(newSequence).forEach((userId) => { + newSequence[userId] = newSequence[userId].filter( + (id: number) => !selectedOrderIds.includes(id) + ) + }) + // Save + updateSequenceMutation.mutate( + { + id: selectedSlotId, + deliverySequence: newSequence, + }, + { + onSuccess: () => { + setSelectedOrderIds([]) + refetchSequence() + window.alert( + 'Success: Orders unassigned successfully' + ) + }, + onError: (error: any) => { + ErrorToast( + `Failed to unassign orders: ${ + error.message || 'Unknown error' + }` + ) + }, + } + ) + } else { + // Assign selected orders to the user + // Update deliverySequence map + const newSequence = { ...(deliverySequence as any) } + // Remove from all other users + Object.keys(newSequence).forEach((userId) => { + newSequence[userId] = newSequence[userId].filter( + (id: number) => !selectedOrderIds.includes(id) + ) + }) + // Add to selected user + if (!newSequence[String(numVal)]) + newSequence[String(numVal)] = [] + newSequence[String(numVal)] = [ + ...new Set([ + ...newSequence[String(numVal)], + ...selectedOrderIds, + ]), + ] + // Save + updateSequenceMutation.mutate( + { + id: selectedSlotId, + deliverySequence: newSequence, + }, + { + onSuccess: () => { + setSelectedOrderIds([]) + refetchSequence() + window.alert( + 'Success: Orders assigned successfully' + ) + }, + onError: (error: any) => { + ErrorToast( + `Failed to assign orders: ${ + error.message || 'Unknown error' + }` + ) + }, + } + ) + } + } + }} + /> +
+ )} + + {/* FAB for Save */} + {hasSequenceChanged && ( +
+ +
+ )} + + ) : ( +
+ + + No slot selected. Please select a slot to view delivery sequence. + + +
+ )} + + {/* Order Menu Dialog */} + setShowOrderMenu(false)} + order={{ + id: selectedOrder?.id || 0, + readableId: selectedOrder?.readableId || 0, + isPackaged: selectedOrder?.isPackaged || false, + isDelivered: selectedOrder?.isDelivered || false, + address: selectedOrder?.address || '', + addressId: selectedOrder?.addressId || 0, + adminNotes: selectedOrder?.adminNotes || null, + latitude: selectedOrder?.latitude || null, + longitude: selectedOrder?.longitude || null, + }} + onViewDetails={() => { + if (selectedOrder) { + navigate({ to: '/dashboard/orders/$id', params: { id: String(selectedOrder.id) } }) + } + setShowOrderMenu(false) + }} + onTogglePackaged={() => { + if (!selectedOrder) return + updatePackagedMutation.mutate( + { orderId: selectedOrder.id.toString(), isPackaged: !selectedOrder.isPackaged }, + { + onSuccess: () => { + refetchOrders() + refetchSequence() + }, + onError: () => { + ErrorToast('Failed to update packaged status') + }, + } + ) + }} + onToggleDelivered={() => { + if (!selectedOrder) return + updateDeliveredMutation.mutate( + { orderId: selectedOrder.id.toString(), isDelivered: !selectedOrder.isDelivered }, + { + onSuccess: () => { + refetchOrders() + refetchSequence() + }, + onError: () => { + ErrorToast('Failed to update delivered status') + }, + } + ) + }} + onOpenAdminNotes={() => { + setShowOrderMenu(false) + setNotesDialogOpen(true) + }} + onCancelOrder={() => { + setShowOrderMenu(false) + setCancelDialogOpen(true) + }} + onAttachLocation={() => {}} + onWhatsApp={() => {}} + onDial={() => {}} + /> + + setCancelDialogOpen(false)} + onSuccess={() => { + refetchOrders() + refetchSequence() + }} + /> + + setNotesDialogOpen(false)}> + { + refetchOrders() + refetchSequence() + setNotesDialogOpen(false) + }} + onCancel={() => setNotesDialogOpen(false)} + /> + +
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.orders.tsx b/apps/admin-web/src/routes/dashboard.orders.tsx new file mode 100644 index 0000000..e1d5435 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.orders.tsx @@ -0,0 +1,99 @@ +import React, { useEffect, useState } from 'react' +import { createFileRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router' +import { ListOrdered, Route as RouteIcon } from 'lucide-react' +import { p as MyText, Dropdown } from 'web-components' +import dayjs from 'dayjs' +import { trpc } from '@/lib/trpc-client' +import { useNavigationTarget } from '@/lib/navigation-target' +import { useMarkDataFetchers } from '@/lib/refresh-context' + +export const Route = createFileRoute('/dashboard/orders')({ + component: ManageOrders, +}) + +function ManageOrders() { + const location = useLocation() + const navigate = useNavigate() + const { getNavigationTarget } = useNavigationTarget() + const [selectedSlotId, setSelectedSlotId] = useState(null) + const { data: slotsData, refetch } = trpc.admin.slots.getAll.useQuery() + + useEffect(() => { + const target = getNavigationTarget() + if (target) { + navigate({ to: target } as any) + } + }, [getNavigationTarget, navigate]) + + useMarkDataFetchers(() => { + refetch() + }) + + if (location.pathname !== '/dashboard/orders') { + return + } + + // Create slot options with Flash Deliveries as first option + const slotOptions = [ + { label: '⚡ Flash Deliveries', value: 'flash' }, + ...(slotsData?.slots?.map((slot) => ({ + label: dayjs(slot.deliveryTime).format('ddd DD MMM, h:mm a'), + value: slot.id.toString() + })) || []) + ] + + const menuItems = [ + { + title: 'Delivery Sequences', + icon: , + color: 'bg-purple-500', + onClick: () => { + if (selectedSlotId === 'flash') { + window.alert('Flash Deliveries: Flash deliveries do not have delivery sequences. Use the Orders menu to manage flash deliveries.') + return + } + navigate({ to: '/dashboard/orders/sequence', search: { slotId: selectedSlotId ?? '' } }) + }, + }, + { + title: 'Orders', + icon: , + color: 'bg-cyan-500', + onClick: () => { + if (selectedSlotId === 'flash') { + navigate({ to: '/dashboard/orders/list', search: { filter: 'flash', slotId: '' } }) + } else { + navigate({ to: '/dashboard/orders/list', search: { filter: '', slotId: selectedSlotId ?? '' } }) + } + }, + }, + ] + + return ( +
+
+ setSelectedSlotId(String(val))} + className="w-full" + /> +
+
+ {menuItems.map((item, index) => ( + + ))} +
+
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.prices.tsx b/apps/admin-web/src/routes/dashboard.prices.tsx new file mode 100644 index 0000000..4c2e005 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.prices.tsx @@ -0,0 +1,420 @@ +import React, { useState, useEffect, useMemo } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { Save, Pencil, EllipsisVertical, Shuffle } from 'lucide-react' +import { + AppContainer, + p as MyText, + pInput as MyTextInput, + BottomDialog, + Checkbox, + MyFlatList, +} from 'web-components' +import MultiSelect from '@/components/MultiSelect' +import { trpc } from '@/lib/trpc-client' + +export const Route = createFileRoute('/dashboard/prices')({ + component: PricesOverview, +}) + +interface SkuItemProps { + sku: any + productName: string + hasChanges: (skuId: number) => boolean + pendingChanges: Record + setPendingChanges: React.Dispatch>> + openEditDialog: (sku: any, productName: string) => void +} + +const SkuItemComponent: React.FC = ({ + sku, + productName, + hasChanges, + pendingChanges, + setPendingChanges, + openEditDialog, +}) => { + const changed = hasChanges(sku.id) + const change = pendingChanges[sku.id] || {} + const displayPrice = change.price !== undefined ? change.price : sku.price + const displayMarketPrice = change.marketPrice !== undefined ? change.marketPrice : sku.marketPrice + const displayFlashPrice = change.flashPrice !== undefined ? change.flashPrice : sku.flashPrice + const displayUnit = (sku.features || []).map((f: any) => f.featureValue).join(' ') || '—' + + return ( +
+
+
+ {changed && ( + + + + )} +
+
+ +
+ + +
+
+ + {productName.length > 20 ? productName.substring(0, 20) + '...' : productName} + + + {sku.displayName || sku.name || ''} + +
+
+ + { + const currentValue = change.isFlashAvailable ?? sku.isFlashAvailable ?? false + setPendingChanges((prev) => ({ + ...prev, + [sku.id]: { + ...prev[sku.id], + isFlashAvailable: !currentValue, + }, + })) + }} + /> + + Flash +
+
+
+ +
+
+ Our Price +
+ ₹{displayPrice} + +
+
+ +
+ Market Price +
+ {displayMarketPrice ? `₹${displayMarketPrice}` : 'N/A'} + +
+
+ +
+ Flash Price +
+ {displayFlashPrice ? `₹${displayFlashPrice}` : 'N/A'} + +
+
+ +
+ Size +
+ {displayUnit} + +
+
+
+
+ ) +} + +interface PendingChange { + price?: number + marketPrice?: number | null + flashPrice?: number | null + isFlashAvailable?: boolean +} + +interface EditDialogState { + open: boolean + sku: any + productName: string + tempPrice: string + tempMarketPrice: string + tempFlashPrice: string +} + +function PricesOverview() { + const navigate = useNavigate() + const [selectedStores, setSelectedStores] = useState([]) + const [pendingChanges, setPendingChanges] = useState>({}) + const [editDialog, setEditDialog] = useState({ + open: false, + sku: null, + productName: '', + tempPrice: '', + tempMarketPrice: '', + tempFlashPrice: '', + }) + const [showMenu, setShowMenu] = useState(false) + + const { data: productsData, isLoading: productsLoading, refetch: refetchProducts } = + trpc.admin.product.getProducts.useQuery() + const { data: storesData, isLoading: storesLoading } = + trpc.admin.store.getStores.useQuery() + + const updatePricesMutation = trpc.admin.product.updateProductPrices.useMutation() + + const stores = storesData?.stores || [] + const allProducts = productsData?.products || [] + + const sortedStores = useMemo(() => + [...stores].sort((a, b) => a.name.localeCompare(b.name)), + [stores] + ) + + const storeOptions = useMemo(() => + sortedStores.map((store) => ({ + label: store.name, + value: store.id.toString(), + })), + [sortedStores] + ) + + useEffect(() => { + if (stores.length > 0 && selectedStores.length === 0) { + setSelectedStores(stores.map((s) => s.id.toString())) + } + }, [stores, selectedStores]) + + const allSkus = useMemo(() => { + const skus: any[] = [] + for (const product of allProducts) { + if (product.skus && product.skus.length > 0) { + for (const sku of product.skus) { + skus.push({ ...sku, _productName: product.name, _storeId: product.storeId }) + } + } + } + return skus + }, [allProducts]) + + const filteredSkus = useMemo(() => { + if (selectedStores.length === 0) return allSkus + return allSkus.filter((sku) => + sku._storeId && selectedStores.includes(sku._storeId.toString()) + ) + }, [allSkus, selectedStores]) + + const hasChanges = (skuId: number) => !!pendingChanges[skuId] + + const openEditDialog = (sku: any, productName: string) => { + const change = pendingChanges[sku.id] || {} + setEditDialog({ + open: true, + sku, + productName, + tempPrice: (change.price ?? sku.price)?.toString() || '', + tempMarketPrice: (change.marketPrice ?? sku.marketPrice)?.toString() || '', + tempFlashPrice: (change.flashPrice ?? sku.flashPrice)?.toString() || '', + }) + } + + const saveEditDialog = () => { + const price = parseFloat(editDialog.tempPrice) + const marketPrice = editDialog.tempMarketPrice ? parseFloat(editDialog.tempMarketPrice) : null + const flashPrice = editDialog.tempFlashPrice ? parseFloat(editDialog.tempFlashPrice) : null + + if (isNaN(price) || price <= 0) { + window.alert('Please enter a valid price') + return + } + + if (editDialog.tempMarketPrice && (isNaN(marketPrice!) || marketPrice! <= 0)) { + window.alert('Please enter a valid market price') + return + } + + if (editDialog.tempFlashPrice && (isNaN(flashPrice!) || flashPrice! <= 0)) { + window.alert('Please enter a valid flash price') + return + } + + setPendingChanges((prev) => ({ + ...prev, + [editDialog.sku.id]: { + price: price !== parseFloat(editDialog.sku.price) ? price : undefined, + marketPrice: marketPrice !== parseFloat(editDialog.sku.marketPrice || '0') ? marketPrice : undefined, + flashPrice: flashPrice !== parseFloat(editDialog.sku.flashPrice || '0') ? flashPrice : undefined, + }, + })) + + } + + const handleSave = () => { + const updates = Object.entries(pendingChanges).map(([skuId, change]) => { + const sku = allSkus.find((s) => s.id === parseInt(skuId)) + const update: any = { productId: sku?.id } + if (change.price !== undefined) update.price = change.price + if (change.marketPrice !== undefined) update.marketPrice = change.marketPrice + if (change.flashPrice !== undefined) update.flashPrice = change.flashPrice + if (change.isFlashAvailable !== undefined) update.isFlashAvailable = change.isFlashAvailable + return update + }) + + updatePricesMutation.mutate( + { updates }, + { + onSuccess: () => { + setPendingChanges({}) + refetchProducts() + window.alert('Prices updated successfully') + }, + onError: (error: any) => { + window.alert(`Failed to update prices: ${error.message || 'Unknown error'}`) + }, + } + ) + } + + const changeCount = Object.keys(pendingChanges).length + + return ( +
+
+
+ setSelectedStores(value as string[])} + placeholder="Select stores" + /> +
+ + +
+ + {productsLoading || storesLoading ? ( +
+
+ Loading... +
+ ) : ( + ( + + )} + keyExtractor={(item) => item.id.toString()} + className="p-4" + /> + )} + + setEditDialog({ ...editDialog, open: false, tempFlashPrice: '', tempMarketPrice: '', tempPrice: '' })}> +
+ {editDialog.productName} + {editDialog.sku?.displayName || editDialog.sku?.name || ''} + +
+ Our Price + setEditDialog({ ...editDialog, tempPrice: e.target.value })} + type="number" + placeholder="Enter price" + /> +
+ +
+ Market Price (Optional) + setEditDialog({ ...editDialog, tempMarketPrice: e.target.value })} + type="number" + placeholder="Enter market price" + /> +
+ +
+ Flash Price (Optional) + setEditDialog({ ...editDialog, tempFlashPrice: e.target.value })} + type="number" + placeholder="Enter flash price" + /> +
+ + +
+
+ + setShowMenu(false)}> +
+ + Options + + +
+
+
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.product-groupings.$id.edit.tsx b/apps/admin-web/src/routes/dashboard.product-groupings.$id.edit.tsx new file mode 100644 index 0000000..6dd80ca --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.product-groupings.$id.edit.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { AppContainer, p as MyText } from 'web-components' +import ProductGroupForm from '../components/ProductGroupForm' +import { trpc } from '@/lib/trpc-client' + +export const Route = createFileRoute('/dashboard/product-groupings/$id/edit')({ + component: EditProductGroup, +}) + +function EditProductGroup() { + const { id } = Route.useParams() + + const groupId = parseInt(id as string) + + const { data: groupsData, isLoading } = trpc.admin.product.getGroups.useQuery() + const groups = groupsData?.groups || [] + const group = groups.find((g) => g.id === groupId) + + if (isLoading) { + return ( + +
+ Loading group... +
+
+ ) + } + + if (!group) { + return ( + +
+ Group not found +
+
+ ) + } + + return ( + + window.history.back()} + onSuccess={() => window.history.back()} + /> + + ) +} diff --git a/apps/admin-web/src/routes/dashboard.product-groupings.new.tsx b/apps/admin-web/src/routes/dashboard.product-groupings.new.tsx new file mode 100644 index 0000000..e7d6649 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.product-groupings.new.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { AppContainer } from 'web-components' +import ProductGroupForm from '../components/ProductGroupForm' + +export const Route = createFileRoute('/dashboard/product-groupings/new')({ + component: CreateProductGroup, +}) + +function CreateProductGroup() { + return ( + + window.history.back()} + onSuccess={() => window.history.back()} + /> + + ) +} diff --git a/apps/admin-web/src/routes/dashboard.product-groupings.tsx b/apps/admin-web/src/routes/dashboard.product-groupings.tsx new file mode 100644 index 0000000..6062b92 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.product-groupings.tsx @@ -0,0 +1,198 @@ +import React, { useState } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { Users, Pencil, Trash2, Package, Clock, Group as GroupIcon, Plus } from 'lucide-react' +import { AppContainer, p as MyText } from 'web-components' +import { useManualRefresh, useMarkDataFetchers } from '@/lib/refresh-context' +import { trpc } from '@/lib/trpc-client' +import { theme } from '@/lib/theme' +import dayjs from 'dayjs' +import type { ProductGroup } from '../components/ProductGroupForm' + +export const Route = createFileRoute('/dashboard/product-groupings')({ + component: ProductGroupings, +}) + +const GroupItem = ({ + group, + onEdit, + onDelete, + onViewProducts, + index, +}: { + group: ProductGroup + onEdit: (group: ProductGroup) => void + onDelete: (id: number) => void + onViewProducts: (products: any[]) => void + index: number +}) => { + return ( +
+
+
+
+
+ +
+
+ + Group Name + + + {group.groupName} + +
+
+ +
+ + +
+
+ + {group.description && ( +
+ + {group.description} + +
+ )} + +
+
+ +
+ + + {dayjs(group.createdAt).format('MMM DD, YYYY')} + +
+
+
+
+
+ ) +} + +function ProductGroupings() { + const navigate = useNavigate() + const { + data: groupsData, + isLoading, + error, + refetch, + } = trpc.admin.product.getGroups.useQuery() + const deleteGroup = trpc.admin.product.deleteGroup.useMutation() + + const groups = groupsData?.groups || [] + const [viewProducts, setViewProducts] = useState(null) + + useManualRefresh(refetch) + useMarkDataFetchers(() => { + refetch() + }) + + const handleCreate = () => { + navigate({ to: '/dashboard/product-groupings/new' }) + } + + const handleEdit = (group: ProductGroup) => { + navigate({ to: '/dashboard/product-groupings/$id/edit', params: { id: String(group.id) } }) + } + + const handleDelete = (id: number) => { + if (window.confirm('Are you sure you want to delete this group?')) { + deleteGroup.mutate( + { id }, + { + onSuccess: () => { + refetch() + }, + } + ) + } + } + + if (isLoading) { + return ( + +
+ Loading product groups... +
+
+ ) + } + + if (error) { + return ( + +
+ Error loading groups +
+
+ ) + } + + return ( +
+
+ {groups.length === 0 ? ( +
+
+ +
+ + No Groups Yet + + + Start by creating your first product group using the button below. + +
+ ) : ( + groups.map((group, index) => ( + + + {index < groups.length - 1 && ( +
+ )} + + )) + )} +
+ +
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.product-tags.edit.tsx b/apps/admin-web/src/routes/dashboard.product-tags.edit.tsx new file mode 100644 index 0000000..9c5e984 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.product-tags.edit.tsx @@ -0,0 +1,118 @@ +import React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { AppContainer, p as MyText, type ImageUploaderNeoItem } from 'web-components' +import TagForm, { type TagFormData as BaseTagFormData } from '../components/TagForm' +import { trpc } from '@/lib/trpc-client' +import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStorage' + +export const Route = createFileRoute('/dashboard/product-tags/edit')({ + validateSearch: (s: Record) => ({ tagId: (s.tagId as string) ?? '' }), + component: EditTag, +}) + +type TagFormData = BaseTagFormData & { existingImageUrl?: string } + +function EditTag() { + const { tagId } = Route.useSearch() + const tagIdNum = tagId ? parseInt(tagId) : null + + const { data: tagData, isLoading: isLoadingTag, error: tagError } = trpc.admin.product.getProductTagById.useQuery( + { id: tagIdNum || 0 }, + { enabled: !!tagIdNum } + ) + const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, { + enabled: false, + }) + const updateTag = trpc.admin.product.updateProductTag.useMutation() + const { data: storesData } = trpc.admin.store.getStores.useQuery() + const { upload, isUploading } = useUploadToObjectStorage() + + const handleSubmit = async (values: TagFormData, images: ImageUploaderNeoItem[], removedExisting: boolean) => { + if (!tagIdNum) return + + try { + let imageUrl: string | null | undefined + let uploadUrls: string[] = [] + + const newImage = images.find((image) => image.mimeType !== null) + if (newImage) { + const response = await fetch(newImage.imgUrl) + const blob = await response.blob() + const result = await upload({ + images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }], + contextString: 'tags', + }) + imageUrl = result.keys[0] + uploadUrls = result.presignedUrls + } else if (removedExisting) { + imageUrl = null + } + + await updateTag.mutateAsync({ + id: tagIdNum, + tagName: values.tagName, + tagDescription: values.tagDescription || undefined, + imageUrl, + isDashboardTag: values.isDashboardTag, + relatedStores: values.relatedStores, + productIds: values.productIds, + uploadUrls, + }) + + await refetchTags() + window.alert('Tag updated successfully') + window.history.back() + } catch (error: any) { + const errorMessage = error.message || 'Failed to update tag' + window.alert(errorMessage) + } + } + + if (isLoadingTag) { + return ( + +
+ Loading tag... +
+
+ ) + } + + if (tagError || !tagData?.tag) { + return ( + +
+ Tag not found +
+
+ ) + } + + const tag = tagData.tag + const tagProductIds = tag.productIds || (tag.products || []).map((p: any) => p.productId) + const orderedProductIds = (tag.sortOrder || []).filter((id: number) => tagProductIds.includes(id)) + const remainingProductIds = tagProductIds.filter((id: number) => !orderedProductIds.includes(id)) + const initialValues: TagFormData = { + tagName: tag.tagName, + tagDescription: tag.tagDescription || '', + isDashboardTag: tag.isDashboardTag, + relatedStores: Array.isArray(tag.relatedStores) ? tag.relatedStores : [], + productIds: [...orderedProductIds, ...remainingProductIds], + existingImageUrl: tag.imageUrl || undefined, + } + + return ( + +
+ ({ id: store.id, name: store.name })) || []} + /> +
+
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.product-tags.new.tsx b/apps/admin-web/src/routes/dashboard.product-tags.new.tsx new file mode 100644 index 0000000..e4046e1 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.product-tags.new.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { AppContainer, type ImageUploaderNeoItem } from 'web-components' +import TagForm, { type TagFormData } from '../components/TagForm' +import { trpc } from '@/lib/trpc-client' +import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStorage' + +export const Route = createFileRoute('/dashboard/product-tags/new')({ + component: AddTag, +}) + +function AddTag() { + const createTag = trpc.admin.product.createProductTag.useMutation() + const { refetch: refetchTags } = trpc.admin.product.getProductTags.useQuery(undefined, { + enabled: false, + }) + const { data: storesData } = trpc.admin.store.getStores.useQuery() + const { upload, isUploading } = useUploadToObjectStorage() + + const handleSubmit = async (values: TagFormData, images: ImageUploaderNeoItem[], _removedExisting: boolean) => { + try { + let imageUrl: string | null | undefined + let uploadUrls: string[] = [] + + const newImage = images.find((image) => image.mimeType !== null) + if (newImage) { + const response = await fetch(newImage.imgUrl) + const blob = await response.blob() + const result = await upload({ + images: [{ blob, mimeType: newImage.mimeType || 'image/jpeg' }], + contextString: 'tags', + }) + imageUrl = result.keys[0] + uploadUrls = result.presignedUrls + } + + await createTag.mutateAsync({ + tagName: values.tagName, + tagDescription: values.tagDescription || undefined, + imageUrl, + isDashboardTag: values.isDashboardTag, + relatedStores: values.relatedStores, + productIds: values.productIds, + uploadUrls, + }) + + await refetchTags() + window.alert('Tag created successfully') + window.history.back() + } catch (error: any) { + const errorMessage = error.message || 'Failed to create tag' + window.alert(errorMessage) + } + } + + const initialValues: TagFormData = { + tagName: '', + tagDescription: '', + isDashboardTag: false, + relatedStores: [], + productIds: [], + } + + return ( + +
+ ({ id: store.id, name: store.name })) || []} + /> +
+
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.product-tags.order.tsx b/apps/admin-web/src/routes/dashboard.product-tags.order.tsx new file mode 100644 index 0000000..7bb2984 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.product-tags.order.tsx @@ -0,0 +1,286 @@ +import React, { useState, useEffect, useCallback } from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { useQueryClient } from '@tanstack/react-query' +import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core' +import { SortableContext, useSortable, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { GripVertical, Tag as TagIcon, ChevronLeft, CircleAlert } from 'lucide-react' +import { AppContainer, p as MyText } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import type { DragOrderItemProps } from '@/types/drag-order-item' + +export const Route = createFileRoute('/dashboard/product-tags/order')({ + component: TagOrders, +}) + +interface Tag { + id: number + tagName: string + imageUrl: string | null +} + +const itemStyle: React.CSSProperties = { + width: '100%', + height: 60, + backgroundColor: 'white', + borderRadius: 8, + borderWidth: 1, + borderStyle: 'solid', + borderColor: '#e5e7eb', + padding: 10, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + boxShadow: '0 1px 2px rgba(0,0,0,0.1)', + marginTop: 4, + marginBottom: 4, +} + +const activeItemStyle: React.CSSProperties = { + ...itemStyle, + borderColor: '#3b82f6', + boxShadow: '0 4px 8px rgba(59,130,246,0.3)', +} + +const SortableTagItem: React.FC> = ({ item, isActive }) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.id }) + const active = isActive || isDragging + + return ( +
+
+ +
+ + {item.imageUrl ? ( + {item.tagName} + ) : ( +
+ +
+ )} + +
+ + {item.tagName} + +
+
+ ) +} + +function TagOrders() { + const queryClient = useQueryClient() + const [tags, setTags] = useState([]) + const [hasChanges, setHasChanges] = useState(false) + + const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery() + const { data: tagsData, isLoading: isLoadingTags, error: tagsError } = trpc.admin.product.getProductTags.useQuery() + const updateConstants = trpc.admin.const.updateConstants.useMutation() + + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })) + + useEffect(() => { + if (tagsData?.tags) { + const tagsOrderConstant = constants?.find((c) => c.key === 'tagsOrder') + + let orderedIds: number[] = [] + + if (tagsOrderConstant) { + const value = tagsOrderConstant.value + + if (Array.isArray(value)) { + orderedIds = value.map((id: any) => parseInt(id)) + } else if (typeof value === 'string') { + orderedIds = value.split(',').map((id: string) => parseInt(id.trim())).filter((id) => !isNaN(id)) + } + } + + const tagMap = new Map(tagsData.tags.map((t) => [t.id, t])) + + const sortedTags: Tag[] = [] + + for (const id of orderedIds) { + const tag = tagMap.get(id) + if (tag) { + sortedTags.push({ + id: tag.id, + tagName: tag.tagName, + imageUrl: tag.imageUrl || null, + }) + tagMap.delete(id) + } + } + + for (const tag of tagMap.values()) { + sortedTags.push({ + id: tag.id, + tagName: tag.tagName, + imageUrl: tag.imageUrl || null, + }) + } + + setTags(sortedTags) + } + }, [constants, tagsData]) + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event + if (over && active.id !== over.id) { + setTags((prev) => { + const oldIndex = prev.findIndex((t) => t.id === active.id) + const newIndex = prev.findIndex((t) => t.id === over.id) + return arrayMove(prev, oldIndex, newIndex) + }) + setHasChanges(true) + } + }, []) + + const handleSave = () => { + const tagIds = tags.map((t) => t.id) + + updateConstants.mutate( + { + constants: [{ + key: 'tagsOrder', + value: tagIds, + }], + }, + { + onSuccess: () => { + setHasChanges(false) + window.alert('Tag order updated successfully!') + queryClient.invalidateQueries({ queryKey: ['const.getConstants'] }) + }, + onError: (error) => { + window.alert('Failed to update tag order. Please try again.') + console.error('Update tag order error:', error) + }, + } + ) + } + + if (isLoadingConstants || isLoadingTags) { + return ( + +
+
+ + Tag Orders +
+
+
+
+ + {isLoadingConstants ? 'Loading order...' : 'Loading tags...'} + +
+
+ + ) + } + + if (constantsError || tagsError) { + return ( + +
+
+ + Tag Orders +
+
+
+ + Error + + {constantsError ? 'Failed to load order' : 'Failed to load tags'} + + +
+
+ + ) + } + + return ( +
+
+ + + Tag Orders + + +
+ + {tags.length === 0 ? ( +
+ + + No tags available + +
+ ) : ( +
+
+ + Long press and drag to reorder • {tags.length} items + +
+ +
+ + t.id)} strategy={verticalListSortingStrategy}> + {tags.map((tag) => ( + {}} isActive={false} /> + ))} + + +
+
+ )} +
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.product-tags.tsx b/apps/admin-web/src/routes/dashboard.product-tags.tsx new file mode 100644 index 0000000..c1e0ac1 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.product-tags.tsx @@ -0,0 +1,142 @@ +import React from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { ArrowDownUp, Plus, CircleAlert, Tags, LayoutDashboard } from 'lucide-react' +import { p as MyText, MyFlatList } from 'web-components' +import { useManualRefresh, useMarkDataFetchers } from '@/lib/refresh-context' +import { TagMenu } from '../components/TagMenu' +import { trpc } from '@/lib/trpc-client' +import type { ProductTagCore } from '@packages/shared' + +export const Route = createFileRoute('/dashboard/product-tags')({ + component: ProductTags, +}) + +interface TagItemData extends ProductTagCore { + id: number + tagDescription: string | null + imageUrl: string | null + isDashboardTag: boolean + createdAt: string | Date +} + +interface TagItemProps { + item: TagItemData + onDeleteSuccess: () => void +} + +const TagItem: React.FC = ({ item, onDeleteSuccess }) => ( +
+
+
+ {item.imageUrl && ( + {item.tagName} + )} +
+ {item.tagName} + {item.tagDescription && ( + {item.tagDescription} + )} + {item.isDashboardTag && ( +
+ + Dashboard Tag +
+ )} +
+
+ +
+
+) + +interface TagHeaderProps { + onAddNewTag: () => void + onOrderTags: () => void +} + +const TagHeader: React.FC = ({ onAddNewTag, onOrderTags }) => ( +
+ + +
+) + +function ProductTags() { + const navigate = useNavigate() + const { data: tagsData, isLoading, error, refetch } = trpc.admin.product.getProductTags.useQuery() + + const tags = tagsData?.tags || [] + + useManualRefresh(refetch) + + useMarkDataFetchers(() => { + refetch() + }) + + const handleAddNewTag = () => { + navigate({ to: '/dashboard/product-tags/new' }) + } + + const handleOrderTags = () => { + navigate({ to: '/dashboard/product-tags/order' }) + } + + if (isLoading) { + return ( +
+ Loading tags... +
+ ) + } + + if (error) { + return ( +
+ + Error + {error?.message || 'Failed to load tags'} + +
+ ) + } + + return ( +
+ } + keyExtractor={(item) => item.id.toString()} + ListHeaderComponent={} + ListEmptyComponent={ +
+ + No tags yet + Create your first product tag +
+ } + className="pb-4" + /> +
+ ) +} diff --git a/apps/admin-web/src/routes/dashboard.products.$id.tsx b/apps/admin-web/src/routes/dashboard.products.$id.tsx new file mode 100644 index 0000000..dbea6e7 --- /dev/null +++ b/apps/admin-web/src/routes/dashboard.products.$id.tsx @@ -0,0 +1,461 @@ +import React, { useState, useRef } from 'react' +import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { + Star, + CircleAlert, + ArrowLeft, + Pencil, + FileText, + LayoutGrid, + Tag, + MessageSquareText, + BadgeCheck, + Reply, + X, + MessageCircle, + Quote, +} from 'lucide-react' +import { AppContainer, p as MyText, BottomDialog, ImageUploader, ImageCarousel } from 'web-components' +import { trpc } from '@/lib/trpc-client' +import { useMarkDataFetchers } from '@/lib/refresh-context' +import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStorage' +import { Formik } from 'formik' + +export const Route = createFileRoute('/dashboard/products/$id')({ + component: ProductDetail, +}) + +interface ReviewResponseFormProps { + reviewId: number + onClose: () => void +} + +const ReviewResponseForm: React.FC = ({ reviewId, onClose }) => { + const [selectedImages, setSelectedImages] = useState<{ blob: Blob, mimeType: string }[]>([]) + const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]) + const fileInputRef = useRef(null) + + const respondToReview = trpc.admin.product.respondToReview.useMutation() + const { upload } = useUploadToObjectStorage() + + const handleFilesSelected = async (files: FileList | null) => { + if (!files || files.length === 0) { + setSelectedImages([]) + setDisplayImages([]) + return + } + const fileArray = Array.from(files) + const blobArray = fileArray.map((file) => ({ blob: file, mimeType: file.type || 'image/jpeg' })) + setSelectedImages(blobArray) + setDisplayImages(fileArray.map((file) => ({ uri: URL.createObjectURL(file) }))) + } + + const handleRemoveImage = (uri: string) => { + const index = displayImages.findIndex((img) => img.uri === uri) + if (index !== -1) { + const newDisplay = displayImages.filter((_, i) => i !== index) + const newFiles = selectedImages.filter((_, i) => i !== index) + setDisplayImages(newDisplay) + setSelectedImages(newFiles) + } + } + + const handleSubmit = async (adminResponse: string) => { + try { + const { keys, presignedUrls } = await upload({ + images: selectedImages, + contextString: 'review', + }) + + await respondToReview.mutateAsync({ + reviewId, + adminResponse, + adminResponseImages: keys, + uploadUrls: presignedUrls, + }) + + window.alert('Response submitted') + onClose() + setSelectedImages([]) + setDisplayImages([]) + } catch (error: any) { + window.alert(error.message || 'Failed to submit response.') + } + } + + return ( + handleSubmit(values.adminResponse)} + > + {({ handleChange, handleSubmit: formikSubmit, values }) => ( +
+