create product
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com'
|
VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com'
|
||||||
VITE_BASE_URL = 'http://192.168.1.122:4000'
|
# VITE_BASE_URL = 'http://192.168.1.122:4000'
|
||||||
VITE_TOKEN_NAME = 'sh_admin_token'
|
VITE_TOKEN_NAME = 'sh_admin_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token'
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
Generated
+659
-1
@@ -9,12 +9,18 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.7",
|
"@headlessui/react": "^2.2.7",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"i18next": "^25.4.2",
|
"i18next": "^25.4.2",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
"rc-rate": "^2.13.1",
|
"rc-rate": "^2.13.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -42,6 +48,7 @@
|
|||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.12",
|
||||||
|
"tw-animate-css": "^1.3.8",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.39.1",
|
"typescript-eslint": "^8.39.1",
|
||||||
"vite": "^7.1.2"
|
"vite": "^7.1.2"
|
||||||
@@ -1156,6 +1163,471 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.8",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1",
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@react-aria/focus": {
|
"node_modules/@react-aria/focus": {
|
||||||
"version": "3.21.1",
|
"version": "3.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz",
|
||||||
@@ -1927,7 +2399,7 @@
|
|||||||
"version": "19.1.9",
|
"version": "19.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz",
|
||||||
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
@@ -2288,6 +2760,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-hidden": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -2486,6 +2970,18 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/classnames": {
|
"node_modules/classnames": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
@@ -2501,6 +2997,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cmdk": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-id": "^1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "^2.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -2629,6 +3141,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-node-es": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -3219,6 +3737,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-nonce": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-proto": {
|
"node_modules/get-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
@@ -3854,6 +4381,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.544.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz",
|
||||||
|
"integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.18",
|
"version": "0.30.18",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
||||||
@@ -4389,6 +4925,53 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.7",
|
||||||
|
"react-style-singleton": "^2.2.3",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.3",
|
||||||
|
"use-sidecar": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-remove-scroll-bar": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-style-singleton": "^2.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.8.2",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz",
|
||||||
@@ -4437,6 +5020,28 @@
|
|||||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-style-singleton": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-nonce": "^1.0.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-toastify": {
|
"node_modules/react-toastify": {
|
||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
|
||||||
@@ -4789,6 +5394,16 @@
|
|||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tw-animate-css": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/Wombosvideo"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@@ -4893,6 +5508,49 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-callback-ref": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/use-sidecar": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-node-es": "^1.1.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||||
|
|||||||
@@ -11,12 +11,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.7",
|
"@headlessui/react": "^2.2.7",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"i18next": "^25.4.2",
|
"i18next": "^25.4.2",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
"rc-rate": "^2.13.1",
|
"rc-rate": "^2.13.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
@@ -44,6 +50,7 @@
|
|||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.12",
|
||||||
|
"tw-animate-css": "^1.3.8",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.39.1",
|
"typescript-eslint": "^8.39.1",
|
||||||
"vite": "^7.1.2"
|
"vite": "^7.1.2"
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||||
@@ -124,4 +124,8 @@ export const Pages = {
|
|||||||
variant: "/category/variant/",
|
variant: "/category/variant/",
|
||||||
attributes: "/category/attributes/",
|
attributes: "/category/attributes/",
|
||||||
},
|
},
|
||||||
|
products: {
|
||||||
|
create: "/products/create",
|
||||||
|
list: "/products/list",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+77
-4
@@ -1,5 +1,23 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getToken } from "./func";
|
import {
|
||||||
|
getToken,
|
||||||
|
getRefreshToken,
|
||||||
|
setToken,
|
||||||
|
setRefreshToken,
|
||||||
|
removeToken,
|
||||||
|
removeRefreshToken,
|
||||||
|
} from "./func";
|
||||||
|
import type {
|
||||||
|
ApiResponse,
|
||||||
|
RefreshTokenResponse,
|
||||||
|
} from "../pages/auth/types/AuthTypes";
|
||||||
|
import { refreshToken } from "../pages/auth/service/AuthService";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
isRefreshingToken?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_BASE_URL,
|
baseURL: import.meta.env.VITE_BASE_URL,
|
||||||
@@ -7,14 +25,69 @@ const axiosInstance = axios.create({
|
|||||||
|
|
||||||
axiosInstance.interceptors.response.use(
|
axiosInstance.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
async (error) => {
|
||||||
|
const originalRequest = error.config;
|
||||||
|
|
||||||
|
if (error?.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
if (window.isRefreshingToken) {
|
||||||
|
// Wait for the refresh to complete
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const checkComplete = setInterval(() => {
|
||||||
|
if (!window.isRefreshingToken) {
|
||||||
|
clearInterval(checkComplete);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
return axiosInstance(originalRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest._retry = true;
|
||||||
|
window.isRefreshingToken = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshTokenValue = await getRefreshToken();
|
||||||
|
if (!refreshTokenValue) {
|
||||||
|
await removeRefreshToken();
|
||||||
|
await removeToken();
|
||||||
|
window.location.href = `/`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data: ApiResponse<RefreshTokenResponse> = await refreshToken({
|
||||||
|
token: refreshTokenValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data?.results?.data?.accessToken?.token) {
|
||||||
|
await setToken(data?.results?.data?.accessToken?.token);
|
||||||
|
if (data?.results?.data?.refreshToken?.token) {
|
||||||
|
await setRefreshToken(data?.results?.data?.refreshToken?.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${data.results.data.accessToken.token}`;
|
||||||
|
return axiosInstance(originalRequest);
|
||||||
|
} else {
|
||||||
|
await removeRefreshToken();
|
||||||
|
await removeToken();
|
||||||
|
window.location.href = `/`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (refreshError) {
|
||||||
|
console.error("Token refresh failed:", refreshError);
|
||||||
|
await removeToken();
|
||||||
|
await removeRefreshToken();
|
||||||
|
// window.location.href = `/`;
|
||||||
|
return Promise.reject(refreshError);
|
||||||
|
} finally {
|
||||||
|
window.isRefreshingToken = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use(async function (config) {
|
axiosInstance.interceptors.request.use(async function (config) {
|
||||||
const token = getToken();
|
const tokenValue = await getToken();
|
||||||
|
const token = tokenValue ? tokenValue : "";
|
||||||
config.headers.Authorization = "Bearer " + token;
|
config.headers.Authorization = "Bearer " + token;
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|||||||
+119
@@ -1,4 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
html {
|
html {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
@@ -60,3 +63,119 @@ textarea::placeholder {
|
|||||||
.tr {
|
.tr {
|
||||||
@apply w-full h-[74px] bg-white border-t border-[#EAEDF5] hover:bg-secondary;
|
@apply w-full h-[74px] bg-white border-t border-[#EAEDF5] hover:bg-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import axios from "../../../config/axios";
|
import axios from "../../../config/axios";
|
||||||
import {
|
import {
|
||||||
|
type ApiResponse,
|
||||||
type CheckHasAccountType,
|
type CheckHasAccountType,
|
||||||
type LoginWithPasswordType,
|
type LoginWithPasswordType,
|
||||||
|
type RefreshTokenResponse,
|
||||||
type RefreshTokenType,
|
type RefreshTokenType,
|
||||||
type RegisterType,
|
type RegisterType,
|
||||||
} from "../types/AuthTypes";
|
} from "../types/AuthTypes";
|
||||||
@@ -21,7 +23,10 @@ export const register = async (params: RegisterType) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refreshToken = async (params: RefreshTokenType) => {
|
export const refreshToken = async (
|
||||||
const { data } = await axios.post(`/auth/refresh`, params);
|
params: RefreshTokenType
|
||||||
|
): Promise<ApiResponse<RefreshTokenResponse>> => {
|
||||||
|
const { data } = await axios.post(`/user/token`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,24 @@ export type RegisterType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RefreshTokenType = {
|
export type RefreshTokenType = {
|
||||||
refreshToken: string;
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TokenInfo = {
|
||||||
|
token: string;
|
||||||
|
expires: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiResponse<T> = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RefreshTokenResponse = {
|
||||||
|
tokenType: string;
|
||||||
|
accessToken: TokenInfo;
|
||||||
|
refreshToken: TokenInfo;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/BrandService";
|
||||||
|
|
||||||
|
// Hook برای دریافت لیست برندها
|
||||||
|
export const useGetBrands = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["brands"],
|
||||||
|
queryFn: api.getBrands,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import { type BrandsResponseType } from "../types/Types";
|
||||||
|
|
||||||
|
// دریافت لیست برندها
|
||||||
|
export const getBrands = async (): Promise<BrandsResponseType> => {
|
||||||
|
const { data } = await axios.get(`/brand`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// تایپهای مربوط به برند
|
||||||
|
|
||||||
|
export type BrandType = {
|
||||||
|
_id: string;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
category: string | null;
|
||||||
|
status: string;
|
||||||
|
description: string;
|
||||||
|
logoUrl: string;
|
||||||
|
images: string[];
|
||||||
|
deleted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PagerType = {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
prevPage: boolean | string;
|
||||||
|
nextPage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrandsResponseType = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
brands: BrandType[];
|
||||||
|
pager: PagerType;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { type FC, useState } from 'react';
|
||||||
|
import { useCreateProductDetail, useCreateProductAttribute, useSaveProduct } from './hooks/useProductData';
|
||||||
|
import { useGetBrands } from '../brand/hooks/useBrandData';
|
||||||
|
import { useGetCategories } from '../category/hooks/useCategoryData';
|
||||||
|
import { StepIndicator, Step1Form, Step2Form, Step3Form } from './components';
|
||||||
|
import { type CreateProductDetailRequestType, type CreateProductAttributeRequestType, type SaveProductRequestType } from './types/Types';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { Pages } from '@/config/Pages';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils';
|
||||||
|
|
||||||
|
const CreateProduct: FC = () => {
|
||||||
|
const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1);
|
||||||
|
const [productData, setProductData] = useState<Partial<CreateProductDetailRequestType & CreateProductAttributeRequestType & SaveProductRequestType>>({});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// API hooks
|
||||||
|
const createProductDetailMutation = useCreateProductDetail();
|
||||||
|
const createProductAttributeMutation = useCreateProductAttribute();
|
||||||
|
const saveProductMutation = useSaveProduct();
|
||||||
|
const { data: brandsData } = useGetBrands();
|
||||||
|
const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories();
|
||||||
|
|
||||||
|
const handleStep1Submit = (data: CreateProductDetailRequestType) => {
|
||||||
|
createProductDetailMutation.mutate(data, {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
setProductData(prev => ({ ...prev, ...data, productId: response?.results?.draftProduct?.id }));
|
||||||
|
setCurrentStep(2);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStep2Submit = (data: Omit<CreateProductAttributeRequestType, 'productId'>) => {
|
||||||
|
const fullData: CreateProductAttributeRequestType = {
|
||||||
|
...data,
|
||||||
|
productId: productData.productId!
|
||||||
|
};
|
||||||
|
|
||||||
|
createProductAttributeMutation.mutate(fullData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setProductData(prev => ({ ...prev, ...fullData }));
|
||||||
|
setCurrentStep(3);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStep3Submit = (data: Omit<SaveProductRequestType, 'productId'>) => {
|
||||||
|
const fullData: SaveProductRequestType = {
|
||||||
|
...data,
|
||||||
|
productId: productData.productId!
|
||||||
|
};
|
||||||
|
|
||||||
|
saveProductMutation.mutate(fullData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('محصول با موفقیت ذخیره شد');
|
||||||
|
navigate(Pages.products.list);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreviousStep = () => {
|
||||||
|
if (currentStep > 1) {
|
||||||
|
setCurrentStep((prev) => (prev - 1) as 1 | 2 | 3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStep1 = () => {
|
||||||
|
// اطمینان از اینکه دادهها درست منتقل میشوند
|
||||||
|
const categories = categoriesData?.results?.data || [];
|
||||||
|
const brands = brandsData?.results?.brands || [];
|
||||||
|
|
||||||
|
// اگر categories در حال loading است، منتظر بمان
|
||||||
|
if (categoriesLoading) {
|
||||||
|
return <div>در حال بارگذاری دستهبندیها...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Step1Form
|
||||||
|
onSubmit={handleStep1Submit}
|
||||||
|
brands={brands}
|
||||||
|
categories={categories}
|
||||||
|
loading={createProductDetailMutation.isPending}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStep2 = () => (
|
||||||
|
<Step2Form
|
||||||
|
onSubmit={handleStep2Submit}
|
||||||
|
loading={createProductAttributeMutation.isPending}
|
||||||
|
onPrevious={handlePreviousStep}
|
||||||
|
categoryId={productData.category}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderStep3 = () => (
|
||||||
|
<Step3Form
|
||||||
|
onSubmit={handleStep3Submit}
|
||||||
|
loading={saveProductMutation.isPending}
|
||||||
|
onPrevious={handlePreviousStep}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">ایجاد محصول جدید</h1>
|
||||||
|
<div className="flex items-center justify-center space-x-8">
|
||||||
|
<StepIndicator step={1} currentStep={currentStep} title="جزئیات محصول" />
|
||||||
|
<StepIndicator step={2} currentStep={currentStep} title="ویژگیها و توضیحات" />
|
||||||
|
<StepIndicator step={3} currentStep={currentStep} title="تصاویر و ذخیره" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
|
||||||
|
{currentStep === 1 && renderStep1()}
|
||||||
|
{currentStep === 2 && renderStep2()}
|
||||||
|
{currentStep === 3 && renderStep3()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateProduct;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
|
||||||
|
const List: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default List
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { type FC, useState, useMemo } from 'react';
|
||||||
|
import Button from '../../../components/Button';
|
||||||
|
import Input from '../../../components/Input';
|
||||||
|
import Select from '../../../components/Select';
|
||||||
|
import Switch from '../../../components/Switch';
|
||||||
|
import RadioGroup from '../../../components/RadioGroup';
|
||||||
|
import { type CreateProductDetailRequestType } from '../types/Types';
|
||||||
|
import { type CategoryTreeNode } from '../../category/types/Types';
|
||||||
|
|
||||||
|
interface Step1FormProps {
|
||||||
|
onSubmit: (data: CreateProductDetailRequestType) => void;
|
||||||
|
brands: { _id: string; title_fa: string }[];
|
||||||
|
categories: CategoryTreeNode[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// تابع برای پیدا کردن همه دستهبندیهای leaf از تمام سطحها
|
||||||
|
const findAllLeafCategories = (categories: CategoryTreeNode[]): CategoryTreeNode[] => {
|
||||||
|
const leafCategories: CategoryTreeNode[] = [];
|
||||||
|
|
||||||
|
const traverse = (nodes: CategoryTreeNode[]) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.leaf) {
|
||||||
|
leafCategories.push(node);
|
||||||
|
}
|
||||||
|
// مهم: همیشه children ها را هم بررسی کن
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
traverse(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
traverse(categories);
|
||||||
|
return leafCategories;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const Step1Form: FC<Step1FormProps> = ({ onSubmit, brands, categories, loading }) => {
|
||||||
|
const [formData, setFormData] = useState<CreateProductDetailRequestType>({
|
||||||
|
category: '',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
title_fa: '',
|
||||||
|
title_en: '',
|
||||||
|
seoTitle: '',
|
||||||
|
source: 'local',
|
||||||
|
isFake: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// پیدا کردن همه دستهبندیهای leaf
|
||||||
|
const leafCategories = useMemo(() => {
|
||||||
|
if (!categories || categories.length === 0) {
|
||||||
|
// اگر دادهای وجود ندارد، لیست خالی برمیگردانیم
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return findAllLeafCategories(categories);
|
||||||
|
}, [categories]);
|
||||||
|
|
||||||
|
// تبدیل به فرمت مورد نیاز Combobox
|
||||||
|
const categoryItems = useMemo(() =>
|
||||||
|
leafCategories.map(cat => ({ value: cat._id, label: cat.title_fa })),
|
||||||
|
[leafCategories]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<Select
|
||||||
|
label="دستهبندی"
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value }))}
|
||||||
|
items={categoryItems}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="برند"
|
||||||
|
value={formData.brand}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, brand: e.target.value }))}
|
||||||
|
items={brands.map(brand => ({ value: brand._id, label: brand.title_fa }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="مدل"
|
||||||
|
value={formData.model}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, model: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="عنوان فارسی"
|
||||||
|
value={formData.title_fa}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, title_fa: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="عنوان انگلیسی"
|
||||||
|
value={formData.title_en}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, title_en: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="عنوان SEO"
|
||||||
|
value={formData.seoTitle}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, seoTitle: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
selected={formData.source}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, source: value as string }))}
|
||||||
|
items={[
|
||||||
|
{ value: 'local', label: 'محلی' },
|
||||||
|
{ value: 'imported', label: 'وارداتی' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
label="محصول تقلبی"
|
||||||
|
active={formData.isFake}
|
||||||
|
onChange={(active) => setFormData(prev => ({ ...prev, isFake: active }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" isLoading={loading}>
|
||||||
|
مرحله بعدی
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Step1Form;
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
import { type FC, useState } from 'react';
|
||||||
|
import Button from '../../../components/Button';
|
||||||
|
import Input from '../../../components/Input';
|
||||||
|
import Textarea from '../../../components/Textarea';
|
||||||
|
import { useGetCategoryAttributes } from '../../category/hooks/useCategoryData';
|
||||||
|
import { type CreateProductAttributeRequestType } from '../types/Types';
|
||||||
|
import { type CategoryAttributeType } from '../../category/types/Types';
|
||||||
|
import { CloseCircle } from 'iconsax-react';
|
||||||
|
|
||||||
|
interface AttributeSelectorProps {
|
||||||
|
attribute: CategoryAttributeType;
|
||||||
|
selectedValues: number[];
|
||||||
|
onChange: (attributeId: number, values: number[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttributeSelector: FC<AttributeSelectorProps> = ({ attribute, selectedValues, onChange }) => {
|
||||||
|
const handleValueChange = (valueId: number, checked: boolean) => {
|
||||||
|
if (attribute.multiple) {
|
||||||
|
// چند انتخابی - checkbox
|
||||||
|
const newValues = checked
|
||||||
|
? [...selectedValues, valueId]
|
||||||
|
: selectedValues.filter(id => id !== valueId);
|
||||||
|
onChange(attribute._id, newValues);
|
||||||
|
} else {
|
||||||
|
// تک انتخابی - radio
|
||||||
|
onChange(attribute._id, checked ? [valueId] : []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 p-4 border border-gray-200 rounded-lg bg-gray-50">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800 mb-2">
|
||||||
|
{attribute.title}
|
||||||
|
{attribute.required && <span className="text-red-500 mr-1">*</span>}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{attribute.hint && (
|
||||||
|
<p className="text-xs text-gray-500 mb-3">{attribute.hint}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{attribute.values.map((value) => {
|
||||||
|
const isChecked = selectedValues.includes(value._id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label key={value._id} className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type={attribute.multiple ? "checkbox" : "radio"}
|
||||||
|
name={`attribute-${attribute._id}`}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={(e) => handleValueChange(value._id, e.target.checked)}
|
||||||
|
className={`w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 ${attribute.multiple ? 'rounded' : 'rounded-full'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">{value.text}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Step2FormProps {
|
||||||
|
onSubmit: (data: Omit<CreateProductAttributeRequestType, 'productId'>) => void;
|
||||||
|
loading: boolean;
|
||||||
|
onPrevious: () => void;
|
||||||
|
categoryId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// کامپوننت مدیریت تگها
|
||||||
|
interface TagInputProps {
|
||||||
|
tags: string[];
|
||||||
|
onChange: (tags: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagInput: FC<TagInputProps> = ({ tags, onChange, placeholder }) => {
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ',') {
|
||||||
|
e.preventDefault();
|
||||||
|
const newTag = inputValue.trim();
|
||||||
|
if (newTag && !tags.includes(newTag)) {
|
||||||
|
onChange([...tags, newTag]);
|
||||||
|
setInputValue('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTag = (tagToRemove: string) => {
|
||||||
|
onChange(tags.filter(tag => tag !== tagToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex flex-wrap gap-2 min-h-[40px] p-2 border border-gray-300 rounded-md focus-within:border-blue-500">
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeTag(tag)}
|
||||||
|
className="hover:bg-blue-200 rounded-full p-0.5"
|
||||||
|
>
|
||||||
|
<CloseCircle size={14} color='red' />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={tags.length === 0 ? placeholder : ''}
|
||||||
|
className="flex-1 min-w-[120px] outline-none bg-transparent text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">برای افزودن تگ، تایپ کنید و Enter یا کاما بزنید</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// کامپوننت مدیریت لیست مزایا/معایب
|
||||||
|
interface ListInputProps {
|
||||||
|
items: string[];
|
||||||
|
onChange: (items: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListInput: FC<ListInputProps> = ({ items, onChange, placeholder, label }) => {
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
|
const addItem = () => {
|
||||||
|
const newItem = inputValue.trim();
|
||||||
|
if (newItem && !items.includes(newItem)) {
|
||||||
|
onChange([...items, newItem]);
|
||||||
|
setInputValue('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeItem = (itemToRemove: string) => {
|
||||||
|
onChange(items.filter(item => item !== itemToRemove));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
addItem();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">{label}</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={addItem}
|
||||||
|
disabled={!inputValue.trim()}
|
||||||
|
className="px-4 w-fit"
|
||||||
|
>
|
||||||
|
افزودن
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div className="space-y-2 flex flex-wrap gap-3 max-h-40 overflow-y-auto">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<div key={index} className="flex min-w-[30%] flex-1 h-10 items-center justify-between p-2 bg-gray-50 rounded-md">
|
||||||
|
<span className="text-sm text-gray-700">{item}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeItem(item)}
|
||||||
|
className="text-red-500 hover:text-red-700 p-1"
|
||||||
|
>
|
||||||
|
<CloseCircle size={16} color='red' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className='min-w-[30%] flex-1 px-2'></div>
|
||||||
|
<div className='min-w-[30%] flex-1 px-2'></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, categoryId }) => {
|
||||||
|
const [formData, setFormData] = useState<Omit<CreateProductAttributeRequestType, 'productId'>>({
|
||||||
|
attributes: [],
|
||||||
|
description: '',
|
||||||
|
metaDescription: '',
|
||||||
|
tags: [],
|
||||||
|
advantages: [],
|
||||||
|
disAdvantages: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: categoryAttributesData } = useGetCategoryAttributes(categoryId || '');
|
||||||
|
|
||||||
|
const handleAttributeChange = (attributeId: number, values: number[]) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
attributes: prev.attributes.filter(attr => attr.id !== attributeId).concat({
|
||||||
|
id: attributeId,
|
||||||
|
values
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* ویژگیهای محصول */}
|
||||||
|
{categoryAttributesData?.results?.data?.category?.attributes && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 mb-4">ویژگیهای محصول</h2>
|
||||||
|
{categoryAttributesData.results.data.category.attributes.map((attribute) => {
|
||||||
|
const selectedAttribute = formData.attributes.find(attr => attr.id === attribute._id);
|
||||||
|
const selectedValues = selectedAttribute?.values || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AttributeSelector
|
||||||
|
key={attribute._id}
|
||||||
|
attribute={attribute}
|
||||||
|
selectedValues={selectedValues}
|
||||||
|
onChange={handleAttributeChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="توضیحات"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
rows={4}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="توضیحات متا"
|
||||||
|
value={formData.metaDescription}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, metaDescription: e.target.value }))}
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">تگها</label>
|
||||||
|
<TagInput
|
||||||
|
tags={formData.tags}
|
||||||
|
onChange={(tags) => setFormData(prev => ({ ...prev, tags }))}
|
||||||
|
placeholder="تگ محصول را وارد کنید..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ListInput
|
||||||
|
label="مزایا"
|
||||||
|
items={formData.advantages}
|
||||||
|
onChange={(advantages) => setFormData(prev => ({ ...prev, advantages }))}
|
||||||
|
placeholder="مزیتی را وارد کنید..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListInput
|
||||||
|
label="معایب"
|
||||||
|
items={formData.disAdvantages}
|
||||||
|
onChange={(disAdvantages) => setFormData(prev => ({ ...prev, disAdvantages }))}
|
||||||
|
placeholder="عیبی را وارد کنید..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2 justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onPrevious}
|
||||||
|
>
|
||||||
|
مرحله قبلی
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={loading}>
|
||||||
|
مرحله بعدی
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Step2Form;
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { type FC, useState } from 'react';
|
||||||
|
import Button from '../../../components/Button';
|
||||||
|
import UploadBoxDraggble from '../../../components/UploadBoxDraggble';
|
||||||
|
import UploadBox from '../../../components/UploadBox';
|
||||||
|
import { useUploadMultiple } from '../../uploader/hooks/useUploaderData';
|
||||||
|
import { type SaveProductRequestType } from '../types/Types';
|
||||||
|
|
||||||
|
interface Step3FormProps {
|
||||||
|
onSubmit: (data: Omit<SaveProductRequestType, 'productId'>) => void;
|
||||||
|
loading: boolean;
|
||||||
|
onPrevious: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Step3Form: FC<Step3FormProps> = ({ onSubmit, loading, onPrevious }) => {
|
||||||
|
|
||||||
|
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||||
|
const [coverImage, setCoverImage] = useState<string>('');
|
||||||
|
const [selectedCoverFile, setSelectedCoverFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const uploadMultipleMutation = useUploadMultiple();
|
||||||
|
|
||||||
|
const handleFileSelect = (files: File[]) => {
|
||||||
|
setSelectedFiles(files);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCoverSelect = (url: string) => {
|
||||||
|
setCoverImage(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCoverFileSelect = (files: File[]) => {
|
||||||
|
setSelectedCoverFile(files.length > 0 ? files[0] : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// اگر هیچ فایلی انتخاب نشده، مستقیم save کن
|
||||||
|
if (selectedFiles.length === 0 && !selectedCoverFile) {
|
||||||
|
const finalData = {
|
||||||
|
imagesList: uploadedImages,
|
||||||
|
coverImage: coverImage || uploadedImages[0] || ''
|
||||||
|
};
|
||||||
|
onSubmit(finalData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// آپلود فایلهای محصول
|
||||||
|
if (selectedFiles.length > 0) {
|
||||||
|
uploadMultipleMutation.mutate(selectedFiles, {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const newUrls = response?.results?.urls?.map(file => file.url) || [];
|
||||||
|
const allImages = [...uploadedImages, ...newUrls];
|
||||||
|
setUploadedImages(allImages);
|
||||||
|
|
||||||
|
// اگر فایل کاور هم انتخاب شده، آن را هم آپلود کن
|
||||||
|
if (selectedCoverFile) {
|
||||||
|
uploadMultipleMutation.mutate([selectedCoverFile], {
|
||||||
|
onSuccess: (coverResponse) => {
|
||||||
|
const coverUrl = coverResponse?.results?.urls?.[0]?.url || '';
|
||||||
|
const finalData = {
|
||||||
|
imagesList: allImages,
|
||||||
|
coverImage: coverUrl || coverImage || allImages[0] || ''
|
||||||
|
};
|
||||||
|
onSubmit(finalData);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Cover upload failed:', error);
|
||||||
|
// حتی اگر کاور آپلود نشد، با تصاویر محصول save کن
|
||||||
|
const finalData = {
|
||||||
|
imagesList: allImages,
|
||||||
|
coverImage: coverImage || allImages[0] || ''
|
||||||
|
};
|
||||||
|
onSubmit(finalData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// فقط تصاویر محصول آپلود شده، save کن
|
||||||
|
const finalData = {
|
||||||
|
imagesList: allImages,
|
||||||
|
coverImage: coverImage || allImages[0] || ''
|
||||||
|
};
|
||||||
|
onSubmit(finalData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Product images upload failed:', error);
|
||||||
|
// میتوانیم اینجا پیام خطا نمایش دهیم
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (selectedCoverFile) {
|
||||||
|
// فقط فایل کاور انتخاب شده
|
||||||
|
uploadMultipleMutation.mutate([selectedCoverFile], {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const coverUrl = response?.results?.urls?.[0]?.url || '';
|
||||||
|
const finalData = {
|
||||||
|
imagesList: uploadedImages,
|
||||||
|
coverImage: coverUrl || coverImage || uploadedImages[0] || ''
|
||||||
|
};
|
||||||
|
onSubmit(finalData);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Cover upload failed:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* اپلود تصویر کاور */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
تصویر کاور محصول
|
||||||
|
</label>
|
||||||
|
<UploadBox
|
||||||
|
label="تصویر کاور را انتخاب کنید"
|
||||||
|
onChange={handleCoverFileSelect}
|
||||||
|
/>
|
||||||
|
{coverImage && (
|
||||||
|
<div className="mt-2 text-sm text-green-600">
|
||||||
|
تصویر کاور انتخاب شده است.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* اپلود تصاویر محصول */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
تصاویر محصول
|
||||||
|
</label>
|
||||||
|
<UploadBoxDraggble
|
||||||
|
label="تصاویر محصول را انتخاب یا اینجا رها کنید"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
isMultiple={true}
|
||||||
|
preview={uploadedImages}
|
||||||
|
onChangePreview={setUploadedImages}
|
||||||
|
getCover={handleCoverSelect}
|
||||||
|
coverUrl={coverImage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-600 mt-2">
|
||||||
|
برای انتخاب تصویر کاور روی یکی از تصاویر کلیک کنید (اختیاری).
|
||||||
|
{coverImage && (
|
||||||
|
<span className="text-green-600 font-medium">
|
||||||
|
{' '}تصویر کاور انتخاب شده است.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onPrevious}
|
||||||
|
>
|
||||||
|
مرحله قبلی
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={loading || uploadMultipleMutation.isPending}
|
||||||
|
disabled={selectedFiles.length === 0 && uploadedImages.length === 0 && !selectedCoverFile && !coverImage}
|
||||||
|
>
|
||||||
|
ایجاد محصول
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Step3Form;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { type FC } from 'react';
|
||||||
|
|
||||||
|
interface StepIndicatorProps {
|
||||||
|
step: number;
|
||||||
|
currentStep: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepIndicator: FC<StepIndicatorProps> = ({ step, currentStep, title }) => {
|
||||||
|
const isCompleted = step < currentStep;
|
||||||
|
const isActive = step === currentStep;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{/* دایره اصلی */}
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${isCompleted
|
||||||
|
? 'bg-gray-900 border-gray-900'
|
||||||
|
: isActive
|
||||||
|
? 'bg-gray-900 border-gray-900'
|
||||||
|
: 'bg-white border-gray-300'
|
||||||
|
}`}>
|
||||||
|
{isCompleted ? (
|
||||||
|
<span className="text-white text-xs">✓</span>
|
||||||
|
) : (
|
||||||
|
<span className={`text-xs font-medium ${isActive ? 'text-white' : 'text-gray-400'}`}>
|
||||||
|
{step}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* متن عنوان */}
|
||||||
|
<span className={`text-sm font-medium ${isCompleted || isActive
|
||||||
|
? 'text-gray-900'
|
||||||
|
: 'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StepIndicator;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as StepIndicator } from "./StepIndicator";
|
||||||
|
export { default as Step1Form } from "./Step1Form";
|
||||||
|
export { default as Step2Form } from "./Step2Form";
|
||||||
|
export { default as Step3Form } from "./Step3Form";
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import * as api from "../service/ProductService";
|
||||||
|
import {
|
||||||
|
type CreateProductDetailRequestType,
|
||||||
|
type CreateProductAttributeRequestType,
|
||||||
|
type SaveProductRequestType
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
// Hook برای مرحله اول: ایجاد جزئیات محصول
|
||||||
|
export const useCreateProductDetail = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CreateProductDetailRequestType) => api.createProductDetail(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook برای مرحله دوم: افزودن ویژگیها و توضیحات
|
||||||
|
export const useCreateProductAttribute = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CreateProductAttributeRequestType) => api.createProductAttribute(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook برای مرحله سوم: ذخیره نهایی محصول
|
||||||
|
export const useSaveProduct = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: SaveProductRequestType) => api.saveProduct(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import {
|
||||||
|
type CreateProductDetailRequestType,
|
||||||
|
type CreateProductDetailResponseType,
|
||||||
|
type CreateProductAttributeRequestType,
|
||||||
|
type CreateProductAttributeResponseType,
|
||||||
|
type SaveProductRequestType,
|
||||||
|
type SaveProductResponseType
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
// مرحله اول: ایجاد جزئیات محصول
|
||||||
|
export const createProductDetail = async (params: CreateProductDetailRequestType): Promise<CreateProductDetailResponseType> => {
|
||||||
|
const { data } = await axios.post(`/admin/products/creation/detail`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// مرحله دوم: افزودن ویژگیها و توضیحات
|
||||||
|
export const createProductAttribute = async (params: CreateProductAttributeRequestType): Promise<CreateProductAttributeResponseType> => {
|
||||||
|
const { data } = await axios.post(`/admin/products/creation/attribute`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// مرحله سوم: ذخیره نهایی محصول
|
||||||
|
export const saveProduct = async (params: SaveProductRequestType): Promise<SaveProductResponseType> => {
|
||||||
|
const { data } = await axios.post(`/admin/products/creation/save`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// تایپهای مربوط به ایجاد محصول سه مرحلهای
|
||||||
|
|
||||||
|
// مرحله اول: جزئیات محصول
|
||||||
|
export type CreateProductDetailRequestType = {
|
||||||
|
category: string;
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
seoTitle: string;
|
||||||
|
source: string;
|
||||||
|
isFake: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DraftProductType = {
|
||||||
|
id: number;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
model: string;
|
||||||
|
nextStep: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateProductDetailResponseType = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
message: string;
|
||||||
|
draftProduct: DraftProductType;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// مرحله دوم: ویژگیها و توضیحات
|
||||||
|
export type ProductAttributeType = {
|
||||||
|
id: number;
|
||||||
|
values: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateProductAttributeRequestType = {
|
||||||
|
productId: number;
|
||||||
|
attributes: ProductAttributeType[];
|
||||||
|
description: string;
|
||||||
|
metaDescription: string;
|
||||||
|
tags: string[];
|
||||||
|
advantages: string[];
|
||||||
|
disAdvantages: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateProductAttributeResponseType = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// مرحله سوم: ذخیره نهایی
|
||||||
|
export type SaveProductRequestType = {
|
||||||
|
productId: number;
|
||||||
|
imagesList: string[];
|
||||||
|
coverImage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SaveProductResponseType = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// تایپ برند
|
||||||
|
export type BrandType = {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrandsResponseType = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
data: BrandType[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// تایپ عمومی محصول
|
||||||
|
export type ProductType = {
|
||||||
|
productId: number;
|
||||||
|
category: string;
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
title_fa: string;
|
||||||
|
title_en: string;
|
||||||
|
seoTitle: string;
|
||||||
|
source: string;
|
||||||
|
isFake: boolean;
|
||||||
|
attributes?: ProductAttributeType[];
|
||||||
|
description?: string;
|
||||||
|
metaDescription?: string;
|
||||||
|
tags?: string[];
|
||||||
|
advantages?: string[];
|
||||||
|
disAdvantages?: string[];
|
||||||
|
imagesList?: string[];
|
||||||
|
coverImage?: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/UploaderService";
|
||||||
|
|
||||||
|
export const useUploadSingle = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: File) => api.uploadSingle(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUploadMultiple = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: File[]) => api.uploadMultiple(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import type { UploadMultipleResponse } from "../types/Types";
|
||||||
|
|
||||||
|
export const uploadSingle = async (params: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", params);
|
||||||
|
const { data } = await axios.post(`/admin/medias/upload/single`, formData);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadMultiple = async (
|
||||||
|
params: File[]
|
||||||
|
): Promise<UploadMultipleResponse> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
params.forEach((file) => {
|
||||||
|
formData.append("file", file);
|
||||||
|
});
|
||||||
|
const { data } = await axios.post<UploadMultipleResponse>(
|
||||||
|
`/admin/medias/upload/multiple`,
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export interface UploadedFile {
|
||||||
|
originalname: string;
|
||||||
|
size: number;
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadMultipleResult {
|
||||||
|
message: string;
|
||||||
|
urls: UploadedFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadMultipleResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: UploadMultipleResult;
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import Update from '../pages/category/Update'
|
|||||||
import { Pages } from '../config/Pages'
|
import { Pages } from '../config/Pages'
|
||||||
import Variant from '../pages/category/Variant'
|
import Variant from '../pages/category/Variant'
|
||||||
import Attributes from '../pages/category/Attributes'
|
import Attributes from '../pages/category/Attributes'
|
||||||
|
import CreateProduct from '../pages/products/Create'
|
||||||
|
import ListProduct from '../pages/products/List'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
@@ -34,6 +36,8 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={`${Pages.category.update}:id`} element={<Update />} />
|
<Route path={`${Pages.category.update}:id`} element={<Update />} />
|
||||||
<Route path={`${Pages.category.variant}:id`} element={<Variant />} />
|
<Route path={`${Pages.category.variant}:id`} element={<Variant />} />
|
||||||
<Route path={`${Pages.category.attributes}:id`} element={<Attributes />} />
|
<Route path={`${Pages.category.attributes}:id`} element={<Attributes />} />
|
||||||
|
<Route path={Pages.products.create} element={<CreateProduct />} />
|
||||||
|
<Route path={Pages.products.list} element={<ListProduct />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const Header: FC = () => {
|
|||||||
// const location = useLocation();
|
// const location = useLocation();
|
||||||
// const [popoverKey, setPopoverKey] = useState(0);
|
// const [popoverKey, setPopoverKey] = useState(0);
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const { setOpenSidebar, openSidebar } = useSharedStore()
|
const { setOpenSidebar, openSidebar, hasSubMenu } = useSharedStore()
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// setPopoverKey((prevKey) => prevKey + 1);
|
// setPopoverKey((prevKey) => prevKey + 1);
|
||||||
@@ -26,7 +26,7 @@ const Header: FC = () => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'>
|
<div className={`fixed z-10 right-4 left-4 top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] ${hasSubMenu ? 'xl:right-[320px] xl:w-[calc(100%-340px)]' : 'xl:right-[285px] xl:w-[calc(100%-305px)]'}`}>
|
||||||
|
|
||||||
<div className='min-w-[270px] hidden xl:block'>
|
<div className='min-w-[270px] hidden xl:block'>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
+7
-1
@@ -23,7 +23,13 @@
|
|||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": false,
|
"erasableSyntaxOnly": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
+14
-2
@@ -1,7 +1,19 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{
|
||||||
{ "path": "./tsconfig.node.json" }
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user