From 2a4608e258ddb4c572fd4165664f3fcb337ecb79 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Mon, 22 Sep 2025 12:24:21 +0330 Subject: [PATCH] create product --- .env | 4 +- components.json | 22 + package-lock.json | 660 +++++++++++++++++- package.json | 7 + src/components/ui/command.tsx | 184 +++++ src/components/ui/dialog.tsx | 141 ++++ src/components/ui/popover.tsx | 46 ++ src/config/Pages.ts | 4 + src/config/axios.ts | 81 ++- src/index.css | 119 ++++ src/lib/utils.ts | 6 + src/pages/auth/service/AuthService.ts | 9 +- src/pages/auth/types/AuthTypes.ts | 21 +- src/pages/brand/hooks/useBrandData.ts | 10 + src/pages/brand/service/BrandService.ts | 8 + src/pages/brand/types/Types.ts | 31 + src/pages/products/Create.tsx | 132 ++++ src/pages/products/List.tsx | 11 + src/pages/products/components/Step1Form.tsx | 143 ++++ src/pages/products/components/Step2Form.tsx | 304 ++++++++ src/pages/products/components/Step3Form.tsx | 172 +++++ .../products/components/StepIndicator.tsx | 42 ++ src/pages/products/components/index.ts | 4 + src/pages/products/hooks/useProductData.ts | 28 + src/pages/products/service/ProductService.ts | 27 + src/pages/products/types/Types.ts | 106 +++ src/pages/uploader/hooks/useUploaderData.ts | 14 + src/pages/uploader/service/UploaderService.ts | 23 + src/pages/uploader/types/Types.ts | 17 + src/router/Main.tsx | 4 + src/shared/Header.tsx | 4 +- tsconfig.app.json | 8 +- tsconfig.json | 20 +- vite.config.ts | 7 + 34 files changed, 2402 insertions(+), 17 deletions(-) create mode 100644 components.json create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/lib/utils.ts create mode 100644 src/pages/brand/hooks/useBrandData.ts create mode 100644 src/pages/brand/service/BrandService.ts create mode 100644 src/pages/brand/types/Types.ts create mode 100644 src/pages/products/Create.tsx create mode 100644 src/pages/products/List.tsx create mode 100644 src/pages/products/components/Step1Form.tsx create mode 100644 src/pages/products/components/Step2Form.tsx create mode 100644 src/pages/products/components/Step3Form.tsx create mode 100644 src/pages/products/components/StepIndicator.tsx create mode 100644 src/pages/products/components/index.ts create mode 100644 src/pages/products/hooks/useProductData.ts create mode 100644 src/pages/products/service/ProductService.ts create mode 100644 src/pages/products/types/Types.ts create mode 100644 src/pages/uploader/hooks/useUploaderData.ts create mode 100644 src/pages/uploader/service/UploaderService.ts create mode 100644 src/pages/uploader/types/Types.ts diff --git a/.env b/.env index 459db48..9111a01 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -# VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com' -VITE_BASE_URL = 'http://192.168.1.122:4000' +VITE_BASE_URL = 'https://shop-api.dev.danakcorp.com' +# VITE_BASE_URL = 'http://192.168.1.122:4000' VITE_TOKEN_NAME = 'sh_admin_token' VITE_REFRESH_TOKEN_NAME = 'sh_admin_refresh_token' \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..2b0833f --- /dev/null +++ b/components.json @@ -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": {} +} diff --git a/package-lock.json b/package-lock.json index 988cde0..1d40d6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,18 @@ "version": "0.0.0", "dependencies": { "@headlessui/react": "^2.2.7", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", "@tailwindcss/vite": "^4.1.12", "@tanstack/react-query": "^5.85.5", "axios": "^1.11.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", "formik": "^2.4.6", "i18next": "^25.4.2", "iconsax-react": "^0.0.8", + "lucide-react": "^0.544.0", "rc-rate": "^2.13.1", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -42,6 +48,7 @@ "globals": "^16.3.0", "postcss": "^8.5.6", "tailwindcss": "^4.1.12", + "tw-animate-css": "^1.3.8", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", "vite": "^7.1.2" @@ -1156,6 +1163,471 @@ "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": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.1.tgz", @@ -1927,7 +2399,7 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -2288,6 +2760,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2486,6 +2970,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": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -2501,6 +2997,22 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2629,6 +3141,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3219,6 +3737,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3854,6 +4381,15 @@ "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": { "version": "0.30.18", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", @@ -4389,6 +4925,53 @@ "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": { "version": "7.8.2", "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" } }, + "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": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", @@ -4789,6 +5394,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "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": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4893,6 +5508,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", diff --git a/package.json b/package.json index c7c3906..f75b320 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,18 @@ }, "dependencies": { "@headlessui/react": "^2.2.7", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", "@tailwindcss/vite": "^4.1.12", "@tanstack/react-query": "^5.85.5", "axios": "^1.11.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", "formik": "^2.4.6", "i18next": "^25.4.2", "iconsax-react": "^0.0.8", + "lucide-react": "^0.544.0", "rc-rate": "^2.13.1", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -44,6 +50,7 @@ "globals": "^16.3.0", "postcss": "^8.5.6", "tailwindcss": "^4.1.12", + "tw-animate-css": "^1.3.8", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", "vite": "^7.1.2" diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..8cb4ca7 --- /dev/null +++ b/src/components/ui/command.tsx @@ -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) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps & { + title?: string + description?: string + className?: string + showCloseButton?: boolean +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..6cb123b --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -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) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..6d51b6c --- /dev/null +++ b/src/components/ui/popover.tsx @@ -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) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 963f2c7..0a0d7d9 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -124,4 +124,8 @@ export const Pages = { variant: "/category/variant/", attributes: "/category/attributes/", }, + products: { + create: "/products/create", + list: "/products/list", + }, }; diff --git a/src/config/axios.ts b/src/config/axios.ts index fd0803b..8e1bdc1 100644 --- a/src/config/axios.ts +++ b/src/config/axios.ts @@ -1,5 +1,23 @@ 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({ baseURL: import.meta.env.VITE_BASE_URL, @@ -7,14 +25,69 @@ const axiosInstance = axios.create({ axiosInstance.interceptors.response.use( (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 = 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); } ); axiosInstance.interceptors.request.use(async function (config) { - const token = getToken(); - + const tokenValue = await getToken(); + const token = tokenValue ? tokenValue : ""; config.headers.Authorization = "Bearer " + token; return config; }); diff --git a/src/index.css b/src/index.css index cd8e80e..cb700bd 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,7 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); html { direction: rtl; @@ -60,3 +63,119 @@ textarea::placeholder { .tr { @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; + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/pages/auth/service/AuthService.ts b/src/pages/auth/service/AuthService.ts index f08a397..5f6d94b 100644 --- a/src/pages/auth/service/AuthService.ts +++ b/src/pages/auth/service/AuthService.ts @@ -1,7 +1,9 @@ import axios from "../../../config/axios"; import { + type ApiResponse, type CheckHasAccountType, type LoginWithPasswordType, + type RefreshTokenResponse, type RefreshTokenType, type RegisterType, } from "../types/AuthTypes"; @@ -21,7 +23,10 @@ export const register = async (params: RegisterType) => { return data; }; -export const refreshToken = async (params: RefreshTokenType) => { - const { data } = await axios.post(`/auth/refresh`, params); +export const refreshToken = async ( + params: RefreshTokenType +): Promise> => { + const { data } = await axios.post(`/user/token`, params); return data; }; + diff --git a/src/pages/auth/types/AuthTypes.ts b/src/pages/auth/types/AuthTypes.ts index a19fbe2..3265406 100644 --- a/src/pages/auth/types/AuthTypes.ts +++ b/src/pages/auth/types/AuthTypes.ts @@ -18,5 +18,24 @@ export type RegisterType = { }; export type RefreshTokenType = { - refreshToken: string; + token: string; +}; + +export type TokenInfo = { + token: string; + expires: string; +}; + +export type ApiResponse = { + status: number; + success: boolean; + results: { + data: T; + }; +}; + +export type RefreshTokenResponse = { + tokenType: string; + accessToken: TokenInfo; + refreshToken: TokenInfo; }; diff --git a/src/pages/brand/hooks/useBrandData.ts b/src/pages/brand/hooks/useBrandData.ts new file mode 100644 index 0000000..633e857 --- /dev/null +++ b/src/pages/brand/hooks/useBrandData.ts @@ -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, + }); +}; diff --git a/src/pages/brand/service/BrandService.ts b/src/pages/brand/service/BrandService.ts new file mode 100644 index 0000000..c38d3a7 --- /dev/null +++ b/src/pages/brand/service/BrandService.ts @@ -0,0 +1,8 @@ +import axios from "../../../config/axios"; +import { type BrandsResponseType } from "../types/Types"; + +// دریافت لیست برندها +export const getBrands = async (): Promise => { + const { data } = await axios.get(`/brand`); + return data; +}; diff --git a/src/pages/brand/types/Types.ts b/src/pages/brand/types/Types.ts new file mode 100644 index 0000000..5c5bd8d --- /dev/null +++ b/src/pages/brand/types/Types.ts @@ -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; + }; +}; diff --git a/src/pages/products/Create.tsx b/src/pages/products/Create.tsx new file mode 100644 index 0000000..8ca1ff1 --- /dev/null +++ b/src/pages/products/Create.tsx @@ -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>({}); + 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) => { + 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) => { + 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
در حال بارگذاری دسته‌بندی‌ها...
; + } + + return ( + + ); + }; + + const renderStep2 = () => ( + + ); + + const renderStep3 = () => ( + + ); + + return ( +
+
+

ایجاد محصول جدید

+
+ + + +
+
+ +
+ {currentStep === 1 && renderStep1()} + {currentStep === 2 && renderStep2()} + {currentStep === 3 && renderStep3()} +
+
+ ); +}; + +export default CreateProduct; \ No newline at end of file diff --git a/src/pages/products/List.tsx b/src/pages/products/List.tsx new file mode 100644 index 0000000..9e487da --- /dev/null +++ b/src/pages/products/List.tsx @@ -0,0 +1,11 @@ +import { type FC } from 'react' + +const List: FC = () => { + return ( +
+ +
+ ) +} + +export default List \ No newline at end of file diff --git a/src/pages/products/components/Step1Form.tsx b/src/pages/products/components/Step1Form.tsx new file mode 100644 index 0000000..fbc2f8a --- /dev/null +++ b/src/pages/products/components/Step1Form.tsx @@ -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 = ({ onSubmit, brands, categories, loading }) => { + const [formData, setFormData] = useState({ + 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 ( +
+
+ setFormData(prev => ({ ...prev, brand: e.target.value }))} + items={brands.map(brand => ({ value: brand._id, label: brand.title_fa }))} + required + /> + + setFormData(prev => ({ ...prev, model: e.target.value }))} + required + /> + + setFormData(prev => ({ ...prev, title_fa: e.target.value }))} + required + /> + + setFormData(prev => ({ ...prev, title_en: e.target.value }))} + required + /> + + setFormData(prev => ({ ...prev, seoTitle: e.target.value }))} + required + /> + + setFormData(prev => ({ ...prev, source: value as string }))} + items={[ + { value: 'local', label: 'محلی' }, + { value: 'imported', label: 'وارداتی' } + ]} + /> + + setFormData(prev => ({ ...prev, isFake: active }))} + /> +
+ +
+ +
+
+ ); +}; + +export default Step1Form; diff --git a/src/pages/products/components/Step2Form.tsx b/src/pages/products/components/Step2Form.tsx new file mode 100644 index 0000000..42fbdc7 --- /dev/null +++ b/src/pages/products/components/Step2Form.tsx @@ -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 = ({ 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 ( +
+

+ {attribute.title} + {attribute.required && *} +

+ + {attribute.hint && ( +

{attribute.hint}

+ )} + +
+ {attribute.values.map((value) => { + const isChecked = selectedValues.includes(value._id); + + return ( + + ); + })} +
+
+ ); +}; + +interface Step2FormProps { + onSubmit: (data: Omit) => void; + loading: boolean; + onPrevious: () => void; + categoryId?: string; +} + +// کامپوننت مدیریت تگ‌ها +interface TagInputProps { + tags: string[]; + onChange: (tags: string[]) => void; + placeholder?: string; +} + +const TagInput: FC = ({ tags, onChange, placeholder }) => { + const [inputValue, setInputValue] = useState(''); + + const handleKeyDown = (e: React.KeyboardEvent) => { + 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 ( +
+
+ {tags.map((tag, index) => ( + + {tag} + + + ))} + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={tags.length === 0 ? placeholder : ''} + className="flex-1 min-w-[120px] outline-none bg-transparent text-sm" + /> +
+

برای افزودن تگ، تایپ کنید و Enter یا کاما بزنید

+
+ ); +}; + +// کامپوننت مدیریت لیست مزایا/معایب +interface ListInputProps { + items: string[]; + onChange: (items: string[]) => void; + placeholder?: string; + label: string; +} + +const ListInput: FC = ({ 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) => { + if (e.key === 'Enter') { + e.preventDefault(); + addItem(); + } + }; + + return ( +
+ +
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + className="flex-1" + /> + +
+ {items.length > 0 && ( +
+ {items.map((item, index) => ( +
+ {item} + +
+ ))} +
+
+
+ )} +
+ ); +}; + +const Step2Form: FC = ({ onSubmit, loading, onPrevious, categoryId }) => { + const [formData, setFormData] = useState>({ + 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 ( +
+ {/* ویژگی‌های محصول */} + {categoryAttributesData?.results?.data?.category?.attributes && ( +
+

ویژگی‌های محصول

+ {categoryAttributesData.results.data.category.attributes.map((attribute) => { + const selectedAttribute = formData.attributes.find(attr => attr.id === attribute._id); + const selectedValues = selectedAttribute?.values || []; + + return ( + + ); + })} +
+ )} + +