2 次代碼提交 1251694556 ... 0f7bac9cbb

作者 SHA1 備註 提交日期
  Mohammad Mahdi Salimi 0f7bac9cbb feat: implmenet posts crud 1 月之前
  Mohammad Mahdi Salimi b9a7bc19fb feat: implement users crud 1 月之前
共有 57 個文件被更改,包括 2451 次插入1945 次删除
  1. 55 48
      package-lock.json
  2. 1 1
      package.json
  3. 80 99
      src/components/layout/data/sidebar-data.ts
  4. 7 16
      src/components/profile-dropdown.tsx
  5. 36 0
      src/constants/users/users-constants.ts
  6. 19 0
      src/core/react-query/keys.ts
  7. 2 195
      src/features/dashboard/index.tsx
  8. 139 0
      src/features/posts/components/data-table-bulk-actions.tsx
  9. 75 0
      src/features/posts/components/data-table-row-actions.tsx
  10. 122 0
      src/features/posts/components/posts-action-dialog.tsx
  11. 118 0
      src/features/posts/components/posts-columns.tsx
  12. 53 0
      src/features/posts/components/posts-delete-dialog.tsx
  13. 27 0
      src/features/posts/components/posts-dialogs.tsx
  14. 103 0
      src/features/posts/components/posts-multi-delete-dialog.tsx
  15. 47 0
      src/features/posts/components/posts-provider.tsx
  16. 174 0
      src/features/posts/components/posts-table.tsx
  17. 110 0
      src/features/posts/components/video-player.tsx
  18. 50 0
      src/features/posts/index.tsx
  19. 1 1
      src/features/users/components/data-table-bulk-actions.tsx
  20. 56 43
      src/features/users/components/data-table-row-actions.tsx
  21. 0 345
      src/features/users/components/users-action-dialog.test.tsx
  22. 574 284
      src/features/users/components/users-action-dialog.tsx
  23. 55 62
      src/features/users/components/users-columns.tsx
  24. 0 178
      src/features/users/components/users-delete-dialog.test.tsx
  25. 16 17
      src/features/users/components/users-delete-dialog.tsx
  26. 30 38
      src/features/users/components/users-dialogs.tsx
  27. 0 157
      src/features/users/components/users-invite-dialog.test.tsx
  28. 0 150
      src/features/users/components/users-invite-dialog.tsx
  29. 0 161
      src/features/users/components/users-multi-delete-dialog.test.tsx
  30. 3 3
      src/features/users/components/users-primary-buttons.tsx
  31. 28 15
      src/features/users/components/users-provider.tsx
  32. 4 24
      src/features/users/components/users-table.tsx
  33. 0 35
      src/features/users/data/data.ts
  34. 0 30
      src/features/users/data/schema.ts
  35. 0 33
      src/features/users/data/users.ts
  36. 11 0
      src/hooks/location/useCities.ts
  37. 10 0
      src/hooks/location/useProvinces.ts
  38. 24 0
      src/hooks/posts/useChangeStatePost.tsx
  39. 12 0
      src/hooks/posts/usePost.tsx
  40. 13 0
      src/hooks/posts/usePosts.tsx
  41. 78 0
      src/hooks/posts/useVideoPlayer.tsx
  42. 17 0
      src/hooks/users/useCreateUser.tsx
  43. 18 0
      src/hooks/users/useDeleteUser.tsx
  44. 24 0
      src/hooks/users/useUpdateUser.tsx
  45. 2 2
      src/hooks/users/useUser.tsx
  46. 11 0
      src/hooks/users/useUserDetail.tsx
  47. 15 0
      src/lib/numberFromSelect.ts
  48. 21 0
      src/routeTree.gen.ts
  49. 6 0
      src/routes/_authenticated/posts/index.tsx
  50. 0 5
      src/routes/_authenticated/users/index.tsx
  51. 20 0
      src/services/location.service.ts
  52. 37 0
      src/services/posts.service.ts
  53. 19 3
      src/services/users.service.ts
  54. 7 0
      src/types/common.types.ts
  55. 27 0
      src/types/location.type.ts
  56. 71 0
      src/types/posts.types.ts
  57. 23 0
      src/types/users.types.ts

+ 55 - 48
package-lock.json

@@ -42,7 +42,7 @@
         "react": "^19.2.5",
         "react-day-picker": "9.14.0",
         "react-dom": "^19.2.5",
-        "react-hook-form": "^7.72.1",
+        "react-hook-form": "^7.75.0",
         "react-top-loading-bar": "^3.0.2",
         "recharts": "^3.8.1",
         "sonner": "^2.0.7",
@@ -113,6 +113,7 @@
       "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@babel/code-frame": "^7.29.0",
         "@babel/generator": "^7.29.0",
@@ -433,27 +434,6 @@
       "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
       "license": "MIT"
     },
-    "node_modules/@emnapi/core": {
-      "version": "1.9.2",
-      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
-      "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "@emnapi/wasi-threads": "1.2.1",
-        "tslib": "^2.4.0"
-      }
-    },
-    "node_modules/@emnapi/runtime": {
-      "version": "1.9.2",
-      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
-      "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "tslib": "^2.4.0"
-      }
-    },
     "node_modules/@emnapi/wasi-threads": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -1105,6 +1085,29 @@
         "node": "^20.19.0 || >=22.12.0"
       }
     },
+    "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+      "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.2.1",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+      "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@oxc-parser/binding-win32-arm64-msvc": {
       "version": "0.127.0",
       "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.127.0.tgz",
@@ -2966,27 +2969,6 @@
         "node": "^20.19.0 || >=22.12.0"
       }
     },
-    "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
-      "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "@emnapi/wasi-threads": "1.2.1",
-        "tslib": "^2.4.0"
-      }
-    },
-    "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
-      "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
-      "license": "MIT",
-      "optional": true,
-      "dependencies": {
-        "tslib": "^2.4.0"
-      }
-    },
     "node_modules/@rolldown/binding-win32-arm64-msvc": {
       "version": "1.0.0-rc.17",
       "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
@@ -3410,6 +3392,7 @@
       "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.168.24.tgz",
       "integrity": "sha512-CQWd9ywDZU6icG65SrjJMzWcNg/ehBKFxfxYssKSuELk0eM9SzJxJ3JBI760kdLXIsXewOX9PHpJDknxC/R5Ag==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@tanstack/history": "1.161.6",
         "@tanstack/react-store": "^0.9.3",
@@ -3499,6 +3482,7 @@
       "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.168.16.tgz",
       "integrity": "sha512-2lkWNMzDWWxVqTf9Y54DH1ceZOrGrOGzx9CFVMq8gQSOxhr3x3+otjVei1Rlx4xmsCc4yCqccqjun5wDtE9MZA==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@tanstack/history": "1.161.6",
         "cookie-es": "^3.0.0",
@@ -3714,6 +3698,7 @@
       "integrity": "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==",
       "dev": true,
       "license": "Apache-2.0",
+      "peer": true,
       "dependencies": {
         "@babel/generator": "^7.28.0",
         "@babel/parser": "^7.28.0",
@@ -3867,6 +3852,7 @@
       "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
       "devOptional": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "undici-types": "~7.19.0"
       }
@@ -3877,6 +3863,7 @@
       "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
       "devOptional": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "csstype": "^3.2.2"
       }
@@ -3887,6 +3874,7 @@
       "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
       "devOptional": true,
       "license": "MIT",
+      "peer": true,
       "peerDependencies": {
         "@types/react": "^19.2.0"
       }
@@ -3942,6 +3930,7 @@
       "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@typescript-eslint/scope-manager": "8.59.0",
         "@typescript-eslint/types": "8.59.0",
@@ -4247,6 +4236,7 @@
       "integrity": "sha512-CWy0lBQJq97nionyJJdnaU4961IXTl43a7UCu5nHy51IoKxAt6PVIJLo+76rVl7KOOgcWHNkG4kbJu/pW7knvA==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@vitest/browser": "4.1.5",
         "@vitest/mocker": "4.1.5",
@@ -4407,6 +4397,7 @@
       "integrity": "sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@vitest/utils": "4.1.5",
         "fflate": "^0.8.2",
@@ -4444,6 +4435,7 @@
       "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -4649,6 +4641,7 @@
         }
       ],
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "baseline-browser-mapping": "^2.10.12",
         "caniuse-lite": "^1.0.30001782",
@@ -4814,7 +4807,8 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
       "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
       "devOptional": true,
-      "license": "MIT"
+      "license": "MIT",
+      "peer": true
     },
     "node_modules/d3-array": {
       "version": "3.2.4",
@@ -5152,6 +5146,7 @@
       "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.8.0",
         "@eslint-community/regexpp": "^4.12.2",
@@ -6114,6 +6109,7 @@
       "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -6886,6 +6882,7 @@
       "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
       "dev": true,
       "license": "MIT",
+      "peer": true,
       "bin": {
         "prettier": "bin/prettier.cjs"
       },
@@ -6999,6 +6996,7 @@
       "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
       "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -7030,6 +7028,7 @@
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
       "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "scheduler": "^0.27.0"
       },
@@ -7038,10 +7037,11 @@
       }
     },
     "node_modules/react-hook-form": {
-      "version": "7.74.0",
-      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.74.0.tgz",
-      "integrity": "sha512-yR6wHr99p9wFv686jhRWVSFhUvDvNbdUf2dKlbno8/VKOCuoNobDGC6S+M2dua9A9Yo8vpcrp8assIYbsZCQ9g==",
+      "version": "7.75.0",
+      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.75.0.tgz",
+      "integrity": "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=18.0.0"
       },
@@ -7065,6 +7065,7 @@
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
       "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "@types/use-sync-external-store": "^0.0.6",
         "use-sync-external-store": "^1.4.0"
@@ -7211,7 +7212,8 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
       "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
-      "license": "MIT"
+      "license": "MIT",
+      "peer": true
     },
     "node_modules/redux-thunk": {
       "version": "3.1.0",
@@ -7298,6 +7300,7 @@
       "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.2.tgz",
       "integrity": "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=10"
       }
@@ -7520,6 +7523,7 @@
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
       "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
       "license": "MIT",
+      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -7607,6 +7611,7 @@
       "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
       "dev": true,
       "license": "Apache-2.0",
+      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -7804,6 +7809,7 @@
       "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
       "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
       "license": "MIT",
+      "peer": true,
       "dependencies": {
         "lightningcss": "^1.32.0",
         "picomatch": "^4.0.4",
@@ -8146,6 +8152,7 @@
       "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
       "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
       "license": "MIT",
+      "peer": true,
       "funding": {
         "url": "https://github.com/sponsors/colinhacks"
       }

+ 1 - 1
package.json

@@ -53,7 +53,7 @@
     "react": "^19.2.5",
     "react-day-picker": "9.14.0",
     "react-dom": "^19.2.5",
-    "react-hook-form": "^7.72.1",
+    "react-hook-form": "^7.75.0",
     "react-top-loading-bar": "^3.0.2",
     "recharts": "^3.8.1",
     "sonner": "^2.0.7",

+ 80 - 99
src/components/layout/data/sidebar-data.ts

@@ -1,23 +1,4 @@
-import {
-  Construction,
-  LayoutDashboard,
-  Monitor,
-  Bug,
-  FileX,
-  HelpCircle,
-  Lock,
-  Bell,
-  Palette,
-  ServerOff,
-  Settings,
-  Wrench,
-  UserCog,
-  UserX,
-  Users,
-  MessagesSquare,
-  ShieldCheck,
-  Play,
-} from 'lucide-react'
+import { LayoutDashboard, Users, ShieldCheck, Play } from 'lucide-react'
 import { type SidebarData } from '../types'
 import Logo from '@/assets/logo.svg'
 
@@ -45,15 +26,15 @@ export const sidebarData: SidebarData = {
         },
         {
           title: 'پست ها',
-          url: '/tasks',
+          url: '/posts',
           icon: Play,
         },
-        {
-          title: 'کامنت ها',
-          url: '/chats',
-          badge: '3',
-          icon: MessagesSquare,
-        },
+        // {
+        //   title: 'کامنت ها',
+        //   url: '/chats',
+        //   badge: '3',
+        //   icon: MessagesSquare,
+        // },
         // {
         //   title: 'Secured by Clerk',
         //   icon: ClerkLogo,
@@ -103,79 +84,79 @@ export const sidebarData: SidebarData = {
             // },
           ],
         },
-        {
-          title: 'خطاها',
-          icon: Bug,
-          items: [
-            {
-              title: 'Unauthorized',
-              url: '/errors/unauthorized',
-              icon: Lock,
-            },
-            {
-              title: 'Forbidden',
-              url: '/errors/forbidden',
-              icon: UserX,
-            },
-            {
-              title: 'Not Found',
-              url: '/errors/not-found',
-              icon: FileX,
-            },
-            {
-              title: 'Internal Server Error',
-              url: '/errors/internal-server-error',
-              icon: ServerOff,
-            },
-            {
-              title: 'Maintenance Error',
-              url: '/errors/maintenance-error',
-              icon: Construction,
-            },
-          ],
-        },
-      ],
-    },
-    {
-      title: 'دیگر',
-      items: [
-        {
-          title: 'Settings',
-          icon: Settings,
-          items: [
-            {
-              title: 'Profile',
-              url: '/settings',
-              icon: UserCog,
-            },
-            {
-              title: 'Account',
-              url: '/settings/account',
-              icon: Wrench,
-            },
-            {
-              title: 'Appearance',
-              url: '/settings/appearance',
-              icon: Palette,
-            },
-            {
-              title: 'Notifications',
-              url: '/settings/notifications',
-              icon: Bell,
-            },
-            {
-              title: 'Display',
-              url: '/settings/display',
-              icon: Monitor,
-            },
-          ],
-        },
-        {
-          title: 'Help Center',
-          url: '/help-center',
-          icon: HelpCircle,
-        },
+        // {
+        //   title: 'خطاها',
+        //   icon: Bug,
+        //   items: [
+        //     {
+        //       title: 'Unauthorized',
+        //       url: '/errors/unauthorized',
+        //       icon: Lock,
+        //     },
+        //     {
+        //       title: 'Forbidden',
+        //       url: '/errors/forbidden',
+        //       icon: UserX,
+        //     },
+        //     {
+        //       title: 'Not Found',
+        //       url: '/errors/not-found',
+        //       icon: FileX,
+        //     },
+        //     {
+        //       title: 'Internal Server Error',
+        //       url: '/errors/internal-server-error',
+        //       icon: ServerOff,
+        //     },
+        //     {
+        //       title: 'Maintenance Error',
+        //       url: '/errors/maintenance-error',
+        //       icon: Construction,
+        //     },
+        //   ],
+        // },
       ],
     },
+    // {
+    //   title: 'دیگر',
+    //   items: [
+    //     {
+    //       title: 'Settings',
+    //       icon: Settings,
+    //       items: [
+    //         {
+    //           title: 'Profile',
+    //           url: '/settings',
+    //           icon: UserCog,
+    //         },
+    //         {
+    //           title: 'Account',
+    //           url: '/settings/account',
+    //           icon: Wrench,
+    //         },
+    //         {
+    //           title: 'Appearance',
+    //           url: '/settings/appearance',
+    //           icon: Palette,
+    //         },
+    //         {
+    //           title: 'Notifications',
+    //           url: '/settings/notifications',
+    //           icon: Bell,
+    //         },
+    //         {
+    //           title: 'Display',
+    //           url: '/settings/display',
+    //           icon: Monitor,
+    //         },
+    //       ],
+    //     },
+    //     {
+    //       title: 'Help Center',
+    //       url: '/help-center',
+    //       icon: HelpCircle,
+    //     },
+    //   ],
+    // },
   ],
 }

+ 7 - 16
src/components/profile-dropdown.tsx

@@ -13,9 +13,11 @@ import {
   DropdownMenuTrigger,
 } from '@/components/ui/dropdown-menu'
 import { SignOutDialog } from '@/components/sign-out-dialog'
+import { useAuthStore } from '@/stores/auth-store'
 
 export function ProfileDropdown() {
   const [open, setOpen] = useDialogState()
+  const { user } = useAuthStore()
 
   return (
     <>
@@ -24,16 +26,18 @@ export function ProfileDropdown() {
           <Button variant='ghost' className='relative h-8 w-8 rounded-full'>
             <Avatar className='h-8 w-8'>
               <AvatarImage src='/avatars/01.png' alt='@shadcn' />
-              <AvatarFallback>SN</AvatarFallback>
+              <AvatarFallback>
+                {user?.username?.slice(0, 2).toUpperCase()}
+              </AvatarFallback>
             </Avatar>
           </Button>
         </DropdownMenuTrigger>
         <DropdownMenuContent className='w-56' align='end' forceMount>
           <DropdownMenuLabel className='font-normal'>
             <div className='flex flex-col gap-1.5'>
-              <p className='text-sm leading-none font-medium'>satnaing</p>
+              <p className='text-sm leading-none font-medium'>{user?.name}</p>
               <p className='text-xs leading-none text-muted-foreground'>
-                satnaingdev@gmail.com
+                {user?.username}
               </p>
             </div>
           </DropdownMenuLabel>
@@ -45,19 +49,6 @@ export function ProfileDropdown() {
                 <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
               </Link>
             </DropdownMenuItem>
-            <DropdownMenuItem asChild>
-              <Link to='/settings'>
-                Billing
-                <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
-              </Link>
-            </DropdownMenuItem>
-            <DropdownMenuItem asChild>
-              <Link to='/settings'>
-                Settings
-                <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
-              </Link>
-            </DropdownMenuItem>
-            <DropdownMenuItem>New Team</DropdownMenuItem>
           </DropdownMenuGroup>
           <DropdownMenuSeparator />
           <DropdownMenuItem variant='destructive' onClick={() => setOpen(true)}>

+ 36 - 0
src/constants/users/users-constants.ts

@@ -0,0 +1,36 @@
+export const genderOption = [
+  { label: 'مرد', value: 0 },
+  { label: 'زن', value: 1 },
+]
+
+export const footOptions = [
+  { label: 'راست', value: 'right' },
+  { label: 'چپ', value: 'left' },
+  { label: 'دو پا', value: 'both' },
+]
+
+export const positionOptions = [
+  { label: 'دروازه‌بان', value: 'GK' },
+  { label: 'مدافع مرکزی', value: 'CB' },
+  { label: 'مدافع راست', value: 'RB' },
+  { label: 'مدافع چپ', value: 'LB' },
+  { label: 'هافبک دفاعی', value: 'DM' },
+  { label: 'هافبک مرکزی', value: 'CM' },
+  { label: 'هافبک هجومی', value: 'AM' },
+  { label: 'وینگر راست', value: 'LW' },
+  { label: 'وینگر چپ', value: 'RW' },
+  { label: 'مهاجم نوک', value: 'CF' },
+  { label: 'مهاجم دوم', value: 'SS' },
+  { label: 'مهاجم (گل زن)', value: 'ST' },
+]
+
+export const skillLevelOptions = [
+  { label: 'مبتدی', value: 'beginner' },
+  { label: 'نیمه حرفه‌ای', value: 'semi-professional' },
+  { label: 'حرفه‌ای', value: 'professional' },
+]
+
+export const activityHistoryOptions = [
+  { label: 'دارم', value: true },
+  { label: 'ندارم', value: false },
+]

+ 19 - 0
src/core/react-query/keys.ts

@@ -1,6 +1,7 @@
 export const queryKeys = {
   users: {
     all: ['users'] as const,
+    user: (userId: number) => ['user', userId] as const,
 
     lists: () => [...queryKeys.users.all, 'list'] as const,
     list: (params: unknown) => [...queryKeys.users.lists(), params] as const,
@@ -9,4 +10,22 @@ export const queryKeys = {
     detail: (id: number | string) =>
       [...queryKeys.users.details(), id] as const,
   },
+
+  posts: {
+    all: ['posts'] as const,
+
+    lists: () => [...queryKeys.posts.all, 'list'] as const,
+    list: (params: unknown) => [...queryKeys.posts.lists(), params] as const,
+
+    details: () => [...queryKeys.posts.all, 'detail'] as const,
+    detail: (id: number | string) =>
+      [...queryKeys.posts.details(), id] as const,
+  },
+
+  location: {
+    all: ['location'] as const,
+    provinces: ['location', 'provinces'] as const,
+    cities: (provinceId: number | string) =>
+      ['location', 'cities', provinceId] as const,
+  },
 }

+ 2 - 195
src/features/dashboard/index.tsx

@@ -1,218 +1,25 @@
-import { Button } from '@/components/ui/button'
-import {
-  Card,
-  CardContent,
-  CardDescription,
-  CardHeader,
-  CardTitle,
-} from '@/components/ui/card'
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import { ConfigDrawer } from '@/components/config-drawer'
 import { Header } from '@/components/layout/header'
 import { Main } from '@/components/layout/main'
-import { TopNav } from '@/components/layout/top-nav'
 import { ProfileDropdown } from '@/components/profile-dropdown'
 import { Search } from '@/components/search'
 import { ThemeSwitch } from '@/components/theme-switch'
-import { Analytics } from './components/analytics'
-import { Overview } from './components/overview'
-import { RecentSales } from './components/recent-sales'
 
 export function Dashboard() {
   return (
     <>
-      {/* ===== Top Heading ===== */}
       <Header>
-        <TopNav links={topNav} className='me-auto' />
-        <Search />
+        <Search className='me-auto' />
         <ThemeSwitch />
         <ConfigDrawer />
         <ProfileDropdown />
       </Header>
 
-      {/* ===== Main ===== */}
       <Main>
         <div className='mb-2 flex items-center justify-between space-y-2'>
-          <h1 className='text-2xl font-bold tracking-tight'>Dashboard</h1>
-          <div className='flex items-center space-x-2'>
-            <Button>Download</Button>
-          </div>
+          <h1 className='text-2xl font-bold tracking-tight'>داشبورد</h1>
         </div>
-        <Tabs
-          orientation='vertical'
-          defaultValue='overview'
-          className='space-y-4'
-        >
-          <div className='w-full overflow-x-auto pb-2'>
-            <TabsList>
-              <TabsTrigger value='overview'>Overview</TabsTrigger>
-              <TabsTrigger value='analytics'>Analytics</TabsTrigger>
-              <TabsTrigger value='reports' disabled>
-                Reports
-              </TabsTrigger>
-              <TabsTrigger value='notifications' disabled>
-                Notifications
-              </TabsTrigger>
-            </TabsList>
-          </div>
-          <TabsContent value='overview' className='space-y-4'>
-            <div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-4'>
-              <Card>
-                <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
-                  <CardTitle className='text-sm font-medium'>
-                    Total Revenue
-                  </CardTitle>
-                  <svg
-                    xmlns='http://www.w3.org/2000/svg'
-                    viewBox='0 0 24 24'
-                    fill='none'
-                    stroke='currentColor'
-                    strokeLinecap='round'
-                    strokeLinejoin='round'
-                    strokeWidth='2'
-                    className='h-4 w-4 text-muted-foreground'
-                  >
-                    <path d='M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' />
-                  </svg>
-                </CardHeader>
-                <CardContent>
-                  <div className='text-2xl font-bold'>$45,231.89</div>
-                  <p className='text-xs text-muted-foreground'>
-                    +20.1% from last month
-                  </p>
-                </CardContent>
-              </Card>
-              <Card>
-                <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
-                  <CardTitle className='text-sm font-medium'>
-                    Subscriptions
-                  </CardTitle>
-                  <svg
-                    xmlns='http://www.w3.org/2000/svg'
-                    viewBox='0 0 24 24'
-                    fill='none'
-                    stroke='currentColor'
-                    strokeLinecap='round'
-                    strokeLinejoin='round'
-                    strokeWidth='2'
-                    className='h-4 w-4 text-muted-foreground'
-                  >
-                    <path d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2' />
-                    <circle cx='9' cy='7' r='4' />
-                    <path d='M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75' />
-                  </svg>
-                </CardHeader>
-                <CardContent>
-                  <div className='text-2xl font-bold'>+2350</div>
-                  <p className='text-xs text-muted-foreground'>
-                    +180.1% from last month
-                  </p>
-                </CardContent>
-              </Card>
-              <Card>
-                <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
-                  <CardTitle className='text-sm font-medium'>Sales</CardTitle>
-                  <svg
-                    xmlns='http://www.w3.org/2000/svg'
-                    viewBox='0 0 24 24'
-                    fill='none'
-                    stroke='currentColor'
-                    strokeLinecap='round'
-                    strokeLinejoin='round'
-                    strokeWidth='2'
-                    className='h-4 w-4 text-muted-foreground'
-                  >
-                    <rect width='20' height='14' x='2' y='5' rx='2' />
-                    <path d='M2 10h20' />
-                  </svg>
-                </CardHeader>
-                <CardContent>
-                  <div className='text-2xl font-bold'>+12,234</div>
-                  <p className='text-xs text-muted-foreground'>
-                    +19% from last month
-                  </p>
-                </CardContent>
-              </Card>
-              <Card>
-                <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
-                  <CardTitle className='text-sm font-medium'>
-                    Active Now
-                  </CardTitle>
-                  <svg
-                    xmlns='http://www.w3.org/2000/svg'
-                    viewBox='0 0 24 24'
-                    fill='none'
-                    stroke='currentColor'
-                    strokeLinecap='round'
-                    strokeLinejoin='round'
-                    strokeWidth='2'
-                    className='h-4 w-4 text-muted-foreground'
-                  >
-                    <path d='M22 12h-4l-3 9L9 3l-3 9H2' />
-                  </svg>
-                </CardHeader>
-                <CardContent>
-                  <div className='text-2xl font-bold'>+573</div>
-                  <p className='text-xs text-muted-foreground'>
-                    +201 since last hour
-                  </p>
-                </CardContent>
-              </Card>
-            </div>
-            <div className='grid grid-cols-1 gap-4 lg:grid-cols-7'>
-              <Card className='col-span-1 lg:col-span-4'>
-                <CardHeader>
-                  <CardTitle>Overview</CardTitle>
-                </CardHeader>
-                <CardContent className='ps-2'>
-                  <Overview />
-                </CardContent>
-              </Card>
-              <Card className='col-span-1 lg:col-span-3'>
-                <CardHeader>
-                  <CardTitle>Recent Sales</CardTitle>
-                  <CardDescription>
-                    You made 265 sales this month.
-                  </CardDescription>
-                </CardHeader>
-                <CardContent>
-                  <RecentSales />
-                </CardContent>
-              </Card>
-            </div>
-          </TabsContent>
-          <TabsContent value='analytics' className='space-y-4'>
-            <Analytics />
-          </TabsContent>
-        </Tabs>
       </Main>
     </>
   )
 }
-
-const topNav = [
-  {
-    title: 'Overview',
-    href: 'dashboard/overview',
-    isActive: true,
-    disabled: false,
-  },
-  {
-    title: 'Customers',
-    href: 'dashboard/customers',
-    isActive: false,
-    disabled: true,
-  },
-  {
-    title: 'Products',
-    href: 'dashboard/products',
-    isActive: false,
-    disabled: true,
-  },
-  {
-    title: 'Settings',
-    href: 'dashboard/settings',
-    isActive: false,
-    disabled: true,
-  },
-]

+ 139 - 0
src/features/posts/components/data-table-bulk-actions.tsx

@@ -0,0 +1,139 @@
+import { useState } from 'react'
+import { type Table } from '@tanstack/react-table'
+import { Trash2, UserX, UserCheck, Mail } from 'lucide-react'
+import { toast } from 'sonner'
+import { sleep } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+} from '@/components/ui/tooltip'
+import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
+import { PostsMultiDeleteDialog } from './posts-multi-delete-dialog'
+import type { Post } from '@/types/posts.types'
+
+type DataTableBulkActionsProps<TData> = {
+  table: Table<TData>
+}
+
+export function DataTableBulkActions<TData>({
+  table,
+}: DataTableBulkActionsProps<TData>) {
+  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
+  const selectedRows = table.getFilteredSelectedRowModel().rows
+
+  const handleBulkStatusChange = (status: 'active' | 'inactive') => {
+    const selectedUsers = selectedRows.map((row) => row.original as Post)
+    toast.promise(sleep(2000), {
+      loading: `${status === 'active' ? 'Activating' : 'Deactivating'} users...`,
+      success: () => {
+        table.resetRowSelection()
+        return `${status === 'active' ? 'Activated' : 'Deactivated'} ${selectedUsers.length} user${selectedUsers.length > 1 ? 's' : ''}`
+      },
+      error: `Error ${status === 'active' ? 'activating' : 'deactivating'} users`,
+    })
+    table.resetRowSelection()
+  }
+
+  const handleBulkInvite = () => {
+    const selectedUsers = selectedRows.map((row) => row.original as Post)
+    toast.promise(sleep(2000), {
+      loading: 'Inviting users...',
+      success: () => {
+        table.resetRowSelection()
+        return `Invited ${selectedUsers.length} user${selectedUsers.length > 1 ? 's' : ''}`
+      },
+      error: 'Error inviting users',
+    })
+    table.resetRowSelection()
+  }
+
+  return (
+    <>
+      <BulkActionsToolbar table={table} entityName='user'>
+        <Tooltip>
+          <TooltipTrigger asChild>
+            <Button
+              variant='outline'
+              size='icon'
+              onClick={handleBulkInvite}
+              className='size-8'
+              aria-label='Invite selected users'
+              title='Invite selected users'
+            >
+              <Mail />
+              <span className='sr-only'>Invite selected users</span>
+            </Button>
+          </TooltipTrigger>
+          <TooltipContent>
+            <p>Invite selected users</p>
+          </TooltipContent>
+        </Tooltip>
+
+        <Tooltip>
+          <TooltipTrigger asChild>
+            <Button
+              variant='outline'
+              size='icon'
+              onClick={() => handleBulkStatusChange('active')}
+              className='size-8'
+              aria-label='Activate selected users'
+              title='Activate selected users'
+            >
+              <UserCheck />
+              <span className='sr-only'>Activate selected users</span>
+            </Button>
+          </TooltipTrigger>
+          <TooltipContent>
+            <p>Activate selected users</p>
+          </TooltipContent>
+        </Tooltip>
+
+        <Tooltip>
+          <TooltipTrigger asChild>
+            <Button
+              variant='outline'
+              size='icon'
+              onClick={() => handleBulkStatusChange('inactive')}
+              className='size-8'
+              aria-label='Deactivate selected users'
+              title='Deactivate selected users'
+            >
+              <UserX />
+              <span className='sr-only'>Deactivate selected users</span>
+            </Button>
+          </TooltipTrigger>
+          <TooltipContent>
+            <p>Deactivate selected users</p>
+          </TooltipContent>
+        </Tooltip>
+
+        <Tooltip>
+          <TooltipTrigger asChild>
+            <Button
+              variant='destructive'
+              size='icon'
+              onClick={() => setShowDeleteConfirm(true)}
+              className='size-8'
+              aria-label='Delete selected users'
+              title='Delete selected users'
+            >
+              <Trash2 />
+              <span className='sr-only'>Delete selected users</span>
+            </Button>
+          </TooltipTrigger>
+          <TooltipContent>
+            <p>Delete selected users</p>
+          </TooltipContent>
+        </Tooltip>
+      </BulkActionsToolbar>
+
+      <PostsMultiDeleteDialog
+        table={table}
+        open={showDeleteConfirm}
+        onOpenChange={setShowDeleteConfirm}
+      />
+    </>
+  )
+}

+ 75 - 0
src/features/posts/components/data-table-row-actions.tsx

@@ -0,0 +1,75 @@
+import { DotsHorizontalIcon } from '@radix-ui/react-icons'
+import { type Row } from '@tanstack/react-table'
+import { Trash2, Eye, EyeOff } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuShortcut,
+  DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu'
+
+import { usePosts } from './posts-provider'
+import { type Post } from '@/types/posts.types'
+
+interface Props {
+  row: Row<Post>
+}
+
+export function DataTableRowActions({ row }: Props) {
+  const { setOpen, setCurrentRow } = usePosts()
+
+  const post = row.original
+
+  const handleChangeState = () => {
+    setCurrentRow(post)
+    setOpen('update')
+  }
+
+  const handleView = () => {
+    setCurrentRow(post)
+    setOpen('view')
+  }
+
+  const handleDelete = () => {
+    setCurrentRow(post)
+    setOpen('delete')
+  }
+
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button variant='ghost' className='flex h-8 w-8 p-0'>
+          <DotsHorizontalIcon className='h-4 w-4' />
+        </Button>
+      </DropdownMenuTrigger>
+
+      <DropdownMenuContent align='end'>
+        <DropdownMenuItem onClick={handleView}>
+          مشاهده
+          <DropdownMenuShortcut>
+            <Eye size={16} />
+          </DropdownMenuShortcut>
+        </DropdownMenuItem>
+
+        <DropdownMenuItem onClick={handleChangeState}>
+          تغییر وضعیت
+          <DropdownMenuShortcut>
+            <EyeOff size={16} />
+          </DropdownMenuShortcut>
+        </DropdownMenuItem>
+
+        <DropdownMenuSeparator />
+
+        <DropdownMenuItem onClick={handleDelete} className='text-red-600'>
+          حذف
+          <DropdownMenuShortcut>
+            <Trash2 size={16} color='red' />
+          </DropdownMenuShortcut>
+        </DropdownMenuItem>
+      </DropdownMenuContent>
+    </DropdownMenu>
+  )
+}

+ 122 - 0
src/features/posts/components/posts-action-dialog.tsx

@@ -0,0 +1,122 @@
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
+
+import { Button } from '@/components/ui/button'
+import { Switch } from '@/components/ui/switch'
+import { Label } from '@/components/ui/label'
+
+import { usePosts } from './posts-provider'
+import { usePost } from '@/hooks/posts/usePost'
+import { useChangeStatePost } from '@/hooks/posts/useChangeStatePost'
+
+import VideoPlayer from './video-player'
+import { useState } from 'react'
+import { type Post } from '@/types/posts.types'
+
+export function PostsActionDialog() {
+  const { open, setOpen, currentRow } = usePosts()
+
+  const isChangeState = open === 'update'
+  const isView = open === 'view'
+  const opened = isChangeState || isView
+  const readOnly = isView
+
+  const { data: post, isLoading } = usePost(currentRow?.id as number)
+  const updateStateMutation = useChangeStatePost()
+
+  const handleClose = () => setOpen(null)
+
+  return (
+    <Dialog open={opened} modal onOpenChange={handleClose}>
+      <DialogContent className='max-h-[90vh] max-w-5xl overflow-y-auto'>
+        <DialogHeader>
+          <DialogTitle>
+            {isChangeState && 'تغییر وضعیت نمایش'}
+            {isView && 'مشاهده پست'}
+          </DialogTitle>
+        </DialogHeader>
+
+        {isLoading || !post ? (
+          <div className='py-10 text-center'>در حال دریافت اطلاعات...</div>
+        ) : (
+          <PostContent
+            key={post.id}
+            post={post}
+            readOnly={readOnly}
+            onClose={handleClose}
+            isPending={updateStateMutation.isPending}
+            onSubmit={(stateValue) =>
+              updateStateMutation.mutate(
+                {
+                  id: post.id,
+                  payload: { state: stateValue ? 1 : 2 },
+                },
+                {
+                  onSuccess: handleClose,
+                }
+              )
+            }
+          />
+        )}
+      </DialogContent>
+    </Dialog>
+  )
+}
+
+type PostContentProps = {
+  post: Post
+  readOnly: boolean
+  onClose: () => void
+  isPending: boolean
+  onSubmit: (value: boolean) => void
+}
+
+function PostContent({
+  post,
+  readOnly,
+  onClose,
+  isPending,
+  onSubmit,
+}: PostContentProps) {
+  const [stateValue, setStateValue] = useState(Number(post.state) === 1)
+
+  return (
+    <div>
+      <div className='pb-2'>
+        <VideoPlayer src={post?.video?.url as string} />
+      </div>
+
+      <div className='py-2 text-sm'>متن پست : {post?.caption}</div>
+
+      <div className='flex items-center justify-between py-4'>
+        <Label>وضعیت : {stateValue ? '✅ در حال نمایش' : '❌ مخفی'}</Label>
+
+        <Switch
+          checked={stateValue}
+          onCheckedChange={setStateValue}
+          disabled={readOnly}
+        />
+      </div>
+
+      {!readOnly && (
+        <div className='flex justify-end gap-2'>
+          <Button type='button' variant='outline' onClick={onClose}>
+            انصراف
+          </Button>
+
+          <Button
+            type='button'
+            onClick={() => onSubmit(stateValue)}
+            disabled={isPending}
+          >
+            تغییر وضعیت
+          </Button>
+        </div>
+      )}
+    </div>
+  )
+}

+ 118 - 0
src/features/posts/components/posts-columns.tsx

@@ -0,0 +1,118 @@
+import { type ColumnDef } from '@tanstack/react-table'
+
+import { cn } from '@/lib/utils'
+
+import { Badge } from '@/components/ui/badge'
+import { Checkbox } from '@/components/ui/checkbox'
+
+import { DataTableColumnHeader } from '@/components/data-table'
+import { LongText } from '@/components/long-text'
+
+import { DataTableRowActions } from './data-table-row-actions'
+import { type Post } from '@/types/posts.types'
+
+export const postsColumns: ColumnDef<Post>[] = [
+  {
+    id: 'select',
+    header: ({ table }) => (
+      <Checkbox
+        checked={
+          table.getIsAllPageRowsSelected() ||
+          (table.getIsSomePageRowsSelected() && 'indeterminate')
+        }
+        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+        aria-label='Select all'
+        className='translate-y-0.5'
+      />
+    ),
+    cell: ({ row }) => (
+      <Checkbox
+        checked={row.getIsSelected()}
+        onCheckedChange={(value) => row.toggleSelected(!!value)}
+        aria-label='Select row'
+        className='translate-y-0.5'
+      />
+    ),
+    enableSorting: false,
+    enableHiding: false,
+    meta: {
+      className: cn('inset-s-0 z-10 rounded-tl-[inherit] max-md:sticky'),
+    },
+  },
+
+  {
+    accessorKey: 'id',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='شناسه' />
+    ),
+    cell: ({ row }) => <div className='w-15'>#{row.original.id}</div>,
+    enableSorting: true,
+  },
+
+  {
+    accessorKey: 'username',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='نام کاربری' />
+    ),
+    cell: ({ row }) => (
+      <LongText className='max-w-36 ps-3'>
+        {row.original.user.username}
+      </LongText>
+    ),
+    enableHiding: false,
+    meta: {
+      className: cn(
+        'drop-shadow-[0_1px_2px_rgb(0_0_0_/_0.1)]',
+        'dark:drop-shadow-[0_1px_2px_rgb(255_255_255_/_0.1)]',
+        'inset-s-6 ps-0.5 max-md:sticky',
+        '@4xl/content:table-cell',
+        '@4xl/content:drop-shadow-none'
+      ),
+    },
+  },
+
+  {
+    accessorKey: 'likes_count',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='تعداد لایک' />
+    ),
+    cell: ({ row }) => <div dir='ltr'>{row.original.likes_count}</div>,
+  },
+
+  {
+    accessorKey: 'comment_count',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='تعداد کامنت' />
+    ),
+    cell: ({ row }) => <div dir='ltr'>{row.original.comment_count}</div>,
+  },
+
+  {
+    accessorKey: 'state',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='وضعیت نمایش' />
+    ),
+    cell: ({ row }) => <Badge variant='secondary'>{row.original.state}</Badge>,
+  },
+
+  {
+    accessorKey: 'created_at',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='تاریخ ایجاد' />
+    ),
+    cell: ({ row }) => {
+      return (
+        <div className='text-sm text-muted-foreground'>
+          {new Date(row.original.created_at).toLocaleDateString('fa-IR')}
+        </div>
+      )
+    },
+  },
+
+  {
+    id: 'actions',
+    cell: DataTableRowActions,
+    enableSorting: false,
+    enableHiding: false,
+  },
+]

+ 53 - 0
src/features/posts/components/posts-delete-dialog.tsx

@@ -0,0 +1,53 @@
+import { AlertTriangle } from 'lucide-react'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { ConfirmDialog } from '@/components/confirm-dialog'
+import { type Post } from '@/types/posts.types'
+
+interface Props {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  currentRow: Post | null
+}
+
+export function PostsDeleteDialog({ open, onOpenChange, currentRow }: Props) {
+  // const { mutate, isPending } = useDeleteUser()
+
+  if (!currentRow) return null
+
+  // const handleDelete = () => {
+  //   mutate(currentRow.id, {
+  //     onSuccess: () => {
+  //       onOpenChange(false)
+  //     },
+  //   })
+  // }
+
+  return (
+    <ConfirmDialog
+      open={open}
+      onOpenChange={onOpenChange}
+      form='posts-delete-form'
+      title={
+        <span className='text-destructive'>
+          <AlertTriangle
+            className='me-1 inline-block stroke-destructive'
+            size={18}
+          />{' '}
+          حذف پست
+        </span>
+      }
+      desc={
+        <Alert variant='destructive'>
+          <AlertTitle>هشدار!</AlertTitle>
+          <AlertDescription>
+            لطفا مراقب باشید، این عملیات قابل برگشت نیست.
+          </AlertDescription>
+        </Alert>
+      }
+      confirmText='حذف'
+      cancelBtnText='انصراف'
+      // isLoading={isPending}
+      destructive
+    />
+  )
+}

+ 27 - 0
src/features/posts/components/posts-dialogs.tsx

@@ -0,0 +1,27 @@
+import { usePosts } from './posts-provider'
+
+import { PostsDeleteDialog } from './posts-delete-dialog'
+import { PostsActionDialog } from './posts-action-dialog'
+
+export function PostsDialogs() {
+  const { open, setOpen, currentRow, setCurrentRow } = usePosts()
+
+  const handleClose = () => {
+    setOpen(null)
+    setCurrentRow(null)
+  }
+
+  return (
+    <>
+      <PostsActionDialog />
+
+      <PostsDeleteDialog
+        open={open === 'delete'}
+        onOpenChange={(state) => {
+          if (!state) handleClose()
+        }}
+        currentRow={currentRow}
+      />
+    </>
+  )
+}

+ 103 - 0
src/features/posts/components/posts-multi-delete-dialog.tsx

@@ -0,0 +1,103 @@
+'use client'
+
+import { useState } from 'react'
+import { type Table } from '@tanstack/react-table'
+import { AlertTriangle } from 'lucide-react'
+import { toast } from 'sonner'
+import { sleep } from '@/lib/utils'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { ConfirmDialog } from '@/components/confirm-dialog'
+
+type PostMultiDeleteDialogProps<TData> = {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  table: Table<TData>
+}
+
+const CONFIRM_WORD = 'DELETE'
+
+export function PostsMultiDeleteDialog<TData>({
+  open,
+  onOpenChange,
+  table,
+}: PostMultiDeleteDialogProps<TData>) {
+  const [value, setValue] = useState('')
+
+  const selectedRows = table.getFilteredSelectedRowModel().rows
+
+  const handleDelete = () => {
+    if (value.trim() !== CONFIRM_WORD) {
+      toast.error(`Please type "${CONFIRM_WORD}" to confirm.`)
+      return
+    }
+
+    onOpenChange(false)
+
+    toast.promise(sleep(2000), {
+      loading: 'Deleting posts...',
+      success: () => {
+        setValue('')
+        table.resetRowSelection()
+        return `Deleted ${selectedRows.length} ${
+          selectedRows.length > 1 ? 'posts' : 'post'
+        }`
+      },
+      error: 'Error',
+    })
+  }
+
+  return (
+    <ConfirmDialog
+      open={open}
+      onOpenChange={onOpenChange}
+      form='posts-multi-delete-form'
+      disabled={value.trim() !== CONFIRM_WORD}
+      title={
+        <span className='text-destructive'>
+          <AlertTriangle
+            className='me-1 inline-block stroke-destructive'
+            size={18}
+          />{' '}
+          Delete {selectedRows.length}{' '}
+          {selectedRows.length > 1 ? 'posts' : 'post'}
+        </span>
+      }
+      desc={
+        <form
+          id='posts-multi-delete-form'
+          onSubmit={(e) => {
+            e.preventDefault()
+            handleDelete()
+          }}
+          className='space-y-4'
+        >
+          <p className='mb-2'>
+            Are you sure you want to delete the selected posts? <br />
+            This action cannot be undone.
+          </p>
+
+          <Label className='my-4 flex flex-col items-start gap-1.5'>
+            <span className=''>Confirm by typing "{CONFIRM_WORD}":</span>
+            <Input
+              value={value}
+              onChange={(e) => setValue(e.target.value)}
+              placeholder={`Type "${CONFIRM_WORD}" to confirm.`}
+              autoFocus
+            />
+          </Label>
+
+          <Alert variant='destructive'>
+            <AlertTitle>Warning!</AlertTitle>
+            <AlertDescription>
+              Please be careful, this operation can not be rolled back.
+            </AlertDescription>
+          </Alert>
+        </form>
+      }
+      confirmText='Delete'
+      destructive
+    />
+  )
+}

+ 47 - 0
src/features/posts/components/posts-provider.tsx

@@ -0,0 +1,47 @@
+import type { Post } from '@/types/posts.types'
+import React, { createContext, useContext, useState } from 'react'
+
+type PostsDialogType = 'update' | 'view' | 'delete' | 'multi-delete'
+
+interface PostsContextType {
+  open: PostsDialogType | null
+  setOpen: (str: PostsDialogType | null) => void
+  currentRow: Post | null
+  setCurrentRow: React.Dispatch<React.SetStateAction<Post | null>>
+}
+
+const PostsContext = createContext<PostsContextType | undefined>(undefined)
+
+interface Props {
+  children: React.ReactNode
+}
+
+export function PostsProvider({ children }: Props) {
+  const [open, setOpen] = useState<PostsDialogType | null>(null)
+
+  const [currentRow, setCurrentRow] = useState<Post | null>(null)
+
+  return (
+    <PostsContext.Provider
+      value={{
+        open,
+        setOpen,
+        currentRow,
+        setCurrentRow,
+      }}
+    >
+      {children}
+    </PostsContext.Provider>
+  )
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export function usePosts() {
+  const context = useContext(PostsContext)
+
+  if (!context) {
+    throw new Error('usePosts must be used within PostsProvider')
+  }
+
+  return context
+}

+ 174 - 0
src/features/posts/components/posts-table.tsx

@@ -0,0 +1,174 @@
+import { useEffect, useState } from 'react'
+import {
+  type SortingState,
+  type VisibilityState,
+  flexRender,
+  getCoreRowModel,
+  getFacetedRowModel,
+  getFacetedUniqueValues,
+  useReactTable,
+} from '@tanstack/react-table'
+import { cn } from '@/lib/utils'
+import { type NavigateFn, useTableUrlState } from '@/hooks/use-table-url-state'
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/components/ui/table'
+import { DataTablePagination, DataTableToolbar } from '@/components/data-table'
+import { DataTableBulkActions } from './data-table-bulk-actions'
+import { postsColumns as columns } from './posts-columns'
+import { type Post } from '@/types/posts.types'
+
+type DataTableProps = {
+  data: Post[]
+  search: Record<string, unknown>
+  navigate: NavigateFn
+}
+
+export function PostsTable({ data, search, navigate }: DataTableProps) {
+  // Local UI-only states
+  const [rowSelection, setRowSelection] = useState({})
+  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
+  const [sorting, setSorting] = useState<SortingState>([])
+
+  // Local state management for table (uncomment to use local-only state, not synced with URL)
+  // const [columnFilters, onColumnFiltersChange] = useState<ColumnFiltersState>([])
+  // const [pagination, onPaginationChange] = useState<PaginationState>({ pageIndex: 0, pageSize: 10 })
+
+  // Synced with URL states (keys/defaults mirror users route search schema)
+  const {
+    columnFilters,
+    onColumnFiltersChange,
+    pagination,
+    onPaginationChange,
+    ensurePageInRange,
+  } = useTableUrlState({
+    search,
+    navigate,
+    pagination: { defaultPage: 1, defaultPageSize: 10 },
+    globalFilter: { enabled: false },
+    columnFilters: [
+      // username per-column text filter
+      { columnId: 'username', searchKey: 'username', type: 'string' },
+      { columnId: 'status', searchKey: 'status', type: 'array' },
+      { columnId: 'role', searchKey: 'role', type: 'array' },
+    ],
+  })
+
+  // eslint-disable-next-line react-hooks/incompatible-library
+  const table = useReactTable({
+    data,
+    columns,
+    state: {
+      sorting,
+      pagination,
+      rowSelection,
+      columnFilters,
+      columnVisibility,
+    },
+    manualPagination: true,
+    manualFiltering: true,
+    manualSorting: true,
+    enableRowSelection: true,
+    onPaginationChange,
+    onColumnFiltersChange,
+    onRowSelectionChange: setRowSelection,
+    onSortingChange: setSorting,
+    onColumnVisibilityChange: setColumnVisibility,
+    getCoreRowModel: getCoreRowModel(),
+    getFacetedRowModel: getFacetedRowModel(),
+    getFacetedUniqueValues: getFacetedUniqueValues(),
+  })
+
+  useEffect(() => {
+    ensurePageInRange(table.getPageCount())
+  }, [table, ensurePageInRange])
+
+  return (
+    <div
+      className={cn(
+        'max-sm:has-[div[role="toolbar"]]:mb-16', // Add margin bottom to the table on mobile when the toolbar is visible
+        'flex flex-1 flex-col gap-4'
+      )}
+    >
+      <DataTableToolbar
+        table={table}
+        searchPlaceholder='فیلتر پست ها...'
+        searchKey='username'
+        filters={[]}
+      />
+      <div className='overflow-hidden rounded-md border'>
+        <Table>
+          <TableHeader>
+            {table.getHeaderGroups().map((headerGroup) => (
+              <TableRow key={headerGroup.id} className='group/row'>
+                {headerGroup.headers.map((header) => {
+                  return (
+                    <TableHead
+                      key={header.id}
+                      colSpan={header.colSpan}
+                      className={cn(
+                        'bg-background group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted',
+                        header.column.columnDef.meta?.className,
+                        header.column.columnDef.meta?.thClassName
+                      )}
+                    >
+                      {header.isPlaceholder
+                        ? null
+                        : flexRender(
+                            header.column.columnDef.header,
+                            header.getContext()
+                          )}
+                    </TableHead>
+                  )
+                })}
+              </TableRow>
+            ))}
+          </TableHeader>
+          <TableBody>
+            {table.getRowModel().rows?.length ? (
+              table.getRowModel().rows.map((row) => (
+                <TableRow
+                  key={row.id}
+                  data-state={row.getIsSelected() && 'selected'}
+                  className='group/row'
+                >
+                  {row.getVisibleCells().map((cell) => (
+                    <TableCell
+                      key={cell.id}
+                      className={cn(
+                        'bg-background group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted',
+                        cell.column.columnDef.meta?.className,
+                        cell.column.columnDef.meta?.tdClassName
+                      )}
+                    >
+                      {flexRender(
+                        cell.column.columnDef.cell,
+                        cell.getContext()
+                      )}
+                    </TableCell>
+                  ))}
+                </TableRow>
+              ))
+            ) : (
+              <TableRow>
+                <TableCell
+                  colSpan={columns?.length}
+                  className='h-24 text-center'
+                >
+                  نتیجه ‌ای یافت نشد.
+                </TableCell>
+              </TableRow>
+            )}
+          </TableBody>
+        </Table>
+      </div>
+      <DataTablePagination table={table} className='mt-auto' />
+      <DataTableBulkActions table={table} />
+    </div>
+  )
+}

+ 110 - 0
src/features/posts/components/video-player.tsx

@@ -0,0 +1,110 @@
+'use client'
+
+import { useVideoPlayer } from '@/hooks/posts/useVideoPlayer'
+import { Play, RefreshCw, Volume2, VolumeX } from 'lucide-react'
+import { useEffect, useRef } from 'react'
+
+type Props = {
+  src: string
+}
+
+export default function VideoPlayer({ src }: Props) {
+  const containerRef = useRef<HTMLDivElement>(null)
+
+  const {
+    videoRef,
+    isPlaying,
+    setIsPlaying,
+    isMuted,
+    progress,
+    isEnded,
+    togglePlay,
+    toggleMute,
+    seek,
+  } = useVideoPlayer()
+
+  useEffect(() => {
+    const container = containerRef.current
+    const video = videoRef.current
+
+    if (!container || !video) return
+
+    const observer = new IntersectionObserver(
+      ([entry]) => {
+        if (!entry?.isIntersecting) {
+          video.pause()
+          setIsPlaying(false)
+        }
+      },
+      { threshold: 0.6 }
+    )
+
+    observer.observe(container)
+
+    return () => observer.disconnect()
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  const handleSeek = (e: React.MouseEvent<HTMLDivElement>) => {
+    const rect = e.currentTarget.getBoundingClientRect()
+    const percent = (e.clientX - rect.left) / rect.width
+    seek(percent)
+  }
+
+  return (
+    <div
+      className='relative mx-auto w-full overflow-hidden rounded-2xl bg-black'
+      ref={containerRef}
+    >
+      <video
+        ref={videoRef}
+        src={src}
+        className='aspect-3/4 w-full object-cover'
+        muted={isMuted}
+        playsInline
+        controls={false}
+        onClick={togglePlay}
+        onContextMenu={(e) => e.preventDefault()}
+      />
+
+      {/* Center Control */}
+      {(isEnded || !isPlaying) && (
+        <div
+          className={`absolute inset-0 flex items-center justify-center bg-black/30 transition-all duration-300`}
+          onClick={togglePlay}
+        >
+          {isEnded ? (
+            <button className='flex size-12 cursor-pointer items-center justify-center rounded-full bg-black/30 transition-transform duration-200 hover:scale-110'>
+              <RefreshCw size={24} className='text-white' />
+            </button>
+          ) : (
+            !isPlaying && (
+              <button className='flex size-12 cursor-pointer items-center justify-center rounded-full bg-black/30 transition-transform duration-200 hover:scale-110'>
+                <Play fill='white' strokeWidth={0} size={24} />
+              </button>
+            )
+          )}
+        </div>
+      )}
+
+      {/* Bottom Controls */}
+      <div className='absolute right-4 bottom-4 left-4 flex items-center justify-between'>
+        <button onClick={toggleMute} className='cursor-pointer text-white'>
+          {isMuted ? <VolumeX size={20} /> : <Volume2 size={20} />}
+        </button>
+      </div>
+
+      {/* Progress */}
+      <div
+        dir='ltr'
+        className='absolute right-0 bottom-0 left-0 h-1.5 cursor-pointer bg-white/20'
+        onClick={handleSeek}
+      >
+        <div
+          className='h-full bg-white transition-all duration-150'
+          style={{ width: `${progress}%` }}
+        />
+      </div>
+    </div>
+  )
+}

+ 50 - 0
src/features/posts/index.tsx

@@ -0,0 +1,50 @@
+import { getRouteApi } from '@tanstack/react-router'
+import { ConfigDrawer } from '@/components/config-drawer'
+import { Header } from '@/components/layout/header'
+import { ProfileDropdown } from '@/components/profile-dropdown'
+import { Search } from '@/components/search'
+import { ThemeSwitch } from '@/components/theme-switch'
+import { PostsProvider } from './components/posts-provider'
+import { Main } from '@/components/layout/main'
+import { PostsTable } from './components/posts-table'
+import { PostsDialogs } from './components/posts-dialogs'
+import { usePosts } from '@/hooks/posts/usePosts'
+
+const route = getRouteApi('/_authenticated/posts/')
+
+export function Posts() {
+  const search = route.useSearch()
+  const navigate = route.useNavigate()
+
+  const { data, isLoading } = usePosts({ page: 1, per_page: 10 })
+  const posts = data?.data?.posts
+
+  return (
+    <PostsProvider>
+      <Header fixed>
+        <Search className='me-auto' />
+        <ThemeSwitch />
+        <ConfigDrawer />
+        <ProfileDropdown />
+      </Header>
+
+      <Main className='flex flex-1 flex-col gap-4 sm:gap-6'>
+        <div className='flex flex-wrap items-end justify-between gap-2'>
+          <div>
+            <h2 className='text-2xl font-bold tracking-tight'>لیست پست ها</h2>
+            <p className='text-muted-foreground'>
+              در اینجا پست ها و وضعیت نمایش آنها را مدیریت کنید.
+            </p>
+          </div>
+        </div>
+        {isLoading ? (
+          <div>در حال دریافت...</div>
+        ) : (
+          <PostsTable data={posts ?? []} search={search} navigate={navigate} />
+        )}
+      </Main>
+
+      <PostsDialogs />
+    </PostsProvider>
+  )
+}

+ 1 - 1
src/features/users/components/data-table-bulk-actions.tsx

@@ -10,8 +10,8 @@ import {
   TooltipTrigger,
 } from '@/components/ui/tooltip'
 import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
-import { type User } from '../data/schema'
 import { UsersMultiDeleteDialog } from './users-multi-delete-dialog'
+import type { User } from '@/types/users.types'
 
 type DataTableBulkActionsProps<TData> = {
   table: Table<TData>

+ 56 - 43
src/features/users/components/data-table-row-actions.tsx

@@ -1,6 +1,6 @@
 import { DotsHorizontalIcon } from '@radix-ui/react-icons'
 import { type Row } from '@tanstack/react-table'
-import { Trash2, UserPen } from 'lucide-react'
+import { Trash2, UserPen, Eye } from 'lucide-react'
 import { Button } from '@/components/ui/button'
 import {
   DropdownMenu,
@@ -10,54 +10,67 @@ import {
   DropdownMenuShortcut,
   DropdownMenuTrigger,
 } from '@/components/ui/dropdown-menu'
-import { type User } from '../data/schema'
+
+import { type User } from '@/types/users.types'
+
 import { useUsers } from './users-provider'
 
-type DataTableRowActionsProps = {
+interface Props {
   row: Row<User>
 }
 
-export function DataTableRowActions({ row }: DataTableRowActionsProps) {
+export function DataTableRowActions({ row }: Props) {
   const { setOpen, setCurrentRow } = useUsers()
+
+  const user = row.original
+
+  const handleEdit = () => {
+    setCurrentRow(user)
+    setOpen('edit')
+  }
+
+  const handleView = () => {
+    setCurrentRow(user)
+    setOpen('view')
+  }
+
+  const handleDelete = () => {
+    setCurrentRow(user)
+    setOpen('delete')
+  }
+
   return (
-    <>
-      <DropdownMenu modal={false}>
-        <DropdownMenuTrigger asChild>
-          <Button
-            variant='ghost'
-            className='flex h-8 w-8 p-0 data-[state=open]:bg-muted'
-          >
-            <DotsHorizontalIcon className='h-4 w-4' />
-            <span className='sr-only'>Open menu</span>
-          </Button>
-        </DropdownMenuTrigger>
-        <DropdownMenuContent align='end' className='w-40'>
-          <DropdownMenuItem
-            onClick={() => {
-              setCurrentRow(row.original)
-              setOpen('edit')
-            }}
-          >
-            ویرایش
-            <DropdownMenuShortcut>
-              <UserPen size={16} />
-            </DropdownMenuShortcut>
-          </DropdownMenuItem>
-          <DropdownMenuSeparator />
-          <DropdownMenuItem
-            onClick={() => {
-              setCurrentRow(row.original)
-              setOpen('delete')
-            }}
-            className='text-red-500!'
-          >
-            حذف
-            <DropdownMenuShortcut>
-              <Trash2 size={16} color='red' />
-            </DropdownMenuShortcut>
-          </DropdownMenuItem>
-        </DropdownMenuContent>
-      </DropdownMenu>
-    </>
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button variant='ghost' className='flex h-8 w-8 p-0'>
+          <DotsHorizontalIcon className='h-4 w-4' />
+        </Button>
+      </DropdownMenuTrigger>
+
+      <DropdownMenuContent align='end'>
+        <DropdownMenuItem onClick={handleView}>
+          مشاهده
+          <DropdownMenuShortcut>
+            <Eye size={16} />
+          </DropdownMenuShortcut>
+        </DropdownMenuItem>
+
+        <DropdownMenuItem onClick={handleEdit}>
+          ویرایش
+          <DropdownMenuShortcut>
+            <UserPen size={16} />
+          </DropdownMenuShortcut>
+        </DropdownMenuItem>
+
+        <DropdownMenuSeparator />
+
+        <DropdownMenuItem onClick={handleDelete} className='text-red-600'>
+          حذف
+          <DropdownMenuShortcut>
+            <Trash2 size={16} color='red' />
+          </DropdownMenuShortcut>
+        </DropdownMenuItem>
+      </DropdownMenuContent>
+    </DropdownMenu>
   )
 }

+ 0 - 345
src/features/users/components/users-action-dialog.test.tsx

@@ -1,345 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render, type RenderResult } from 'vitest-browser-react'
-import { type UserEvent, userEvent } from 'vitest/browser'
-import { showSubmittedData } from '@/lib/show-submitted-data'
-import { type User } from '../data/schema'
-import { UsersActionDialog } from './users-action-dialog'
-
-const VALIDATION_MESSAGES = {
-  firstName: 'First Name is required.',
-  lastName: 'Last Name is required.',
-  username: 'Username is required.',
-  phoneNumber: 'Phone number is required.',
-  email: 'Email is required.',
-  role: 'Role is required.',
-  password: 'Password is required.',
-  passwordMismatch: "Passwords don't match.",
-  passwordLength: 'Password must be at least 8 characters long.',
-  passwordNumber: 'Password must contain at least one number.',
-  passwordLowercase: 'Password must contain at least one lowercase letter.',
-} as const
-
-const MOCK_USER: User = {
-  id: 'alex_uuid',
-  firstName: 'Alex',
-  lastName: 'Smith',
-  username: 'alex_smith',
-  email: 'alex@smith.com',
-  phoneNumber: '+19999999999',
-  status: 'active',
-  role: 'superadmin',
-  createdAt: new Date('2026-01-01'),
-  updatedAt: new Date('2026-02-02'),
-}
-
-vi.mock('@/lib/show-submitted-data', () => ({ showSubmittedData: vi.fn() }))
-
-describe('UsersActionDialog', () => {
-  beforeEach(() => vi.clearAllMocks())
-
-  describe('add user', () => {
-    it('renders title and description', async () => {
-      const { getByRole, getByText } = await render(
-        <UsersActionDialog open onOpenChange={vi.fn()} />
-      )
-
-      const title = getByRole('heading', {
-        level: 2,
-        name: /Add New User/i,
-      })
-      const description = getByText(
-        /Create new user here. Click save when you're done./i
-      )
-
-      await expect.element(title).toBeInTheDocument()
-      await expect.element(description).toBeInTheDocument()
-    })
-
-    it('shows validation messages when the form is submitted with empty fields', async () => {
-      const { getByRole, getByText } = await render(
-        <UsersActionDialog open onOpenChange={vi.fn()} />
-      )
-
-      const submitButton = getByRole('button', { name: /Save Changes/i })
-      await userEvent.click(submitButton)
-
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.firstName))
-        .toBeInTheDocument()
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.lastName))
-        .toBeInTheDocument()
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.username))
-        .toBeInTheDocument()
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.phoneNumber))
-        .toBeInTheDocument()
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.email))
-        .toBeInTheDocument()
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.role))
-        .toBeInTheDocument()
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.password))
-        .toBeInTheDocument()
-    })
-
-    it('keeps confirm password disabled until password field is touched', async () => {
-      const { getByRole } = await render(
-        <UsersActionDialog open onOpenChange={vi.fn()} />
-      )
-
-      const password = getByRole('textbox', { name: /^Password$/i })
-      const confirmPassword = getByRole('textbox', {
-        name: /Confirm Password/i,
-      })
-      await expect.element(confirmPassword).toBeDisabled()
-
-      await userEvent.type(password, 'a')
-      await expect.element(confirmPassword).toBeEnabled()
-    })
-
-    it('shows password validation messages when password is invalid', async () => {
-      const { getByRole, getByText } = await render(
-        <UsersActionDialog open onOpenChange={vi.fn()} />
-      )
-
-      const password = getByRole('textbox', { name: /^Password$/i })
-      const confirmPassword = getByRole('textbox', {
-        name: /Confirm Password/i,
-      })
-      await userEvent.type(password, 'a')
-      await userEvent.type(confirmPassword, 'b')
-      const submitButton = getByRole('button', { name: /Save Changes/i })
-
-      await userEvent.click(submitButton)
-
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.passwordMismatch))
-        .toBeInTheDocument()
-
-      await userEvent.fill(password, 'short')
-
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.passwordLength))
-        .toBeInTheDocument()
-
-      await userEvent.fill(password, 'ONLYUPPERCASE')
-
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.passwordLowercase))
-        .toBeInTheDocument()
-
-      await userEvent.fill(password, 'onlylowercase')
-
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.passwordNumber))
-        .toBeInTheDocument()
-
-      await userEvent.fill(password, 'S3cur3P@ssw0rd')
-      await userEvent.fill(confirmPassword, 'S3cur3P@ssw0rd')
-
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.passwordMismatch))
-        .not.toBeInTheDocument()
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.passwordLength))
-        .not.toBeInTheDocument()
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.passwordNumber))
-        .not.toBeInTheDocument()
-    })
-
-    it('shows the submitted data when the form is submitted successfully', async () => {
-      const onOpenChange = vi.fn()
-
-      const screen = await render(
-        <UsersActionDialog open onOpenChange={onOpenChange} />
-      )
-
-      await fillRequiredProfileFields(userEvent, screen, MOCK_USER)
-
-      await fillPasswords(userEvent, screen, 'S3cur3P@ssw0rd', 'S3cur3P@ssw0rd')
-
-      const submitButton = screen.getByRole('button', { name: /Save Changes/i })
-      await userEvent.click(submitButton)
-
-      expect(onOpenChange).toHaveBeenCalledOnce()
-      expect(onOpenChange).toHaveBeenCalledWith(false)
-
-      expect(showSubmittedData).toHaveBeenCalledOnce()
-      expect(showSubmittedData).toHaveBeenCalledWith({
-        firstName: MOCK_USER.firstName,
-        lastName: MOCK_USER.lastName,
-        username: MOCK_USER.username,
-        email: MOCK_USER.email,
-        role: MOCK_USER.role,
-        phoneNumber: MOCK_USER.phoneNumber,
-        password: 'S3cur3P@ssw0rd',
-        confirmPassword: 'S3cur3P@ssw0rd',
-        isEdit: false,
-      })
-    })
-  })
-
-  describe('edit user', () => {
-    it('renders title and description', async () => {
-      const { getByRole, getByText } = await render(
-        <UsersActionDialog open onOpenChange={vi.fn()} currentRow={MOCK_USER} />
-      )
-
-      const title = getByRole('heading', {
-        level: 2,
-        name: /Edit User/i,
-      })
-      const description = getByText(
-        /Update the user here\. Click save when you're done\./i
-      )
-
-      await expect.element(title).toBeInTheDocument()
-      await expect.element(description).toBeInTheDocument()
-    })
-
-    it('submits without password changes', async () => {
-      const onOpenChange = vi.fn()
-      const screen = await render(
-        <UsersActionDialog
-          open
-          onOpenChange={onOpenChange}
-          currentRow={MOCK_USER}
-        />
-      )
-
-      const submitButton = screen.getByRole('button', { name: /Save Changes/i })
-      await userEvent.click(submitButton)
-
-      expect(onOpenChange).toHaveBeenCalledOnce()
-      expect(onOpenChange).toHaveBeenCalledWith(false)
-
-      expect(showSubmittedData).toHaveBeenCalledOnce()
-      expect(showSubmittedData).toHaveBeenCalledWith({
-        firstName: MOCK_USER.firstName,
-        lastName: MOCK_USER.lastName,
-        username: MOCK_USER.username,
-        email: MOCK_USER.email,
-        phoneNumber: MOCK_USER.phoneNumber,
-        role: MOCK_USER.role,
-        password: '',
-        confirmPassword: '',
-        isEdit: true,
-      })
-    })
-
-    it('requires confirm password when password is changed', async () => {
-      const { getByRole, getByText } = await render(
-        <UsersActionDialog open onOpenChange={vi.fn()} currentRow={MOCK_USER} />
-      )
-
-      const password = getByRole('textbox', { name: /^Password$/i })
-      const confirmPassword = getByRole('textbox', {
-        name: /Confirm Password/i,
-      })
-
-      await userEvent.fill(password, 'S3cur3P@ssw0rd')
-      await expect.element(confirmPassword).toBeEnabled()
-
-      const submitButton = getByRole('button', { name: /Save Changes/i })
-      await userEvent.click(submitButton)
-
-      await expect
-        .element(getByText(VALIDATION_MESSAGES.passwordMismatch))
-        .toBeInTheDocument()
-    })
-
-    it('shows the submitted data when the form is submitted successfully', async () => {
-      const onOpenChange = vi.fn()
-      const screen = await render(
-        <UsersActionDialog
-          open
-          onOpenChange={onOpenChange}
-          currentRow={MOCK_USER}
-        />
-      )
-
-      const EDIT_SUCCESS_FIRST_NAME = 'John'
-      const EDIT_SUCCESS_PASSWORD = 'S3cur3P@ssw0rd'
-
-      await userEvent.fill(
-        screen.getByLabelText(/first name/i),
-        EDIT_SUCCESS_FIRST_NAME
-      )
-      await fillPasswords(
-        userEvent,
-        screen,
-        EDIT_SUCCESS_PASSWORD,
-        EDIT_SUCCESS_PASSWORD
-      )
-
-      const submitButton = screen.getByRole('button', { name: /Save Changes/i })
-      await userEvent.click(submitButton)
-
-      expect(onOpenChange).toHaveBeenCalledOnce()
-      expect(onOpenChange).toHaveBeenCalledWith(false)
-
-      expect(showSubmittedData).toHaveBeenCalledOnce()
-      expect(showSubmittedData).toHaveBeenCalledWith({
-        firstName: EDIT_SUCCESS_FIRST_NAME,
-        lastName: MOCK_USER.lastName,
-        username: MOCK_USER.username,
-        email: MOCK_USER.email,
-        phoneNumber: MOCK_USER.phoneNumber,
-        role: MOCK_USER.role,
-        password: EDIT_SUCCESS_PASSWORD,
-        confirmPassword: EDIT_SUCCESS_PASSWORD,
-        isEdit: true,
-      })
-    })
-  })
-})
-
-async function fillRequiredProfileFields(
-  user: UserEvent,
-  screen: RenderResult,
-  overrides?: {
-    firstName?: string
-    lastName?: string
-    username?: string
-    email?: string
-    roleOption?: string | RegExp
-    phoneNumber?: string
-  }
-) {
-  const entries = [
-    [/first name/i, overrides?.firstName ?? 'John'],
-    [/last name/i, overrides?.lastName ?? 'Doe'],
-    [/username/i, overrides?.username ?? 'john_doe'],
-    [/^email$/i, overrides?.email ?? 'a@b.co'],
-    [/phone number/i, overrides?.phoneNumber ?? '+19999999999'],
-  ] as const
-
-  for (const [label, value] of entries) {
-    const el = screen.getByLabelText(label)
-    await expect.element(el).toBeInTheDocument()
-    await user.fill(el, value)
-  }
-
-  const roleSelect = screen.getByRole('combobox', { name: /Role/i })
-  await user.click(roleSelect)
-  await user.click(
-    screen.getByRole('option', { name: overrides?.roleOption ?? 'Superadmin' })
-  )
-}
-
-async function fillPasswords(
-  user: UserEvent,
-  screen: RenderResult,
-  a: string,
-  b: string
-) {
-  const password = screen.getByLabelText(/^Password$/i)
-  const confirmPassword = screen.getByLabelText(/^Confirm Password$/i)
-  await user.fill(password, a)
-  await user.fill(confirmPassword, b)
-}

+ 574 - 284
src/features/users/components/users-action-dialog.tsx

@@ -1,18 +1,12 @@
-'use client'
-
-import { z } from 'zod'
 import { useForm } from 'react-hook-form'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { showSubmittedData } from '@/lib/show-submitted-data'
-import { Button } from '@/components/ui/button'
+
 import {
   Dialog,
   DialogContent,
-  DialogDescription,
-  DialogFooter,
   DialogHeader,
   DialogTitle,
 } from '@/components/ui/dialog'
+
 import {
   Form,
   FormControl,
@@ -21,305 +15,601 @@ import {
   FormLabel,
   FormMessage,
 } from '@/components/ui/form'
+
 import { Input } from '@/components/ui/input'
-import { PasswordInput } from '@/components/password-input'
-import { SelectDropdown } from '@/components/select-dropdown'
-import { roles } from '../data/data'
-import { type User } from '../data/schema'
-
-const formSchema = z
-  .object({
-    firstName: z.string().min(1, 'First Name is required.'),
-    lastName: z.string().min(1, 'Last Name is required.'),
-    username: z.string().min(1, 'Username is required.'),
-    phoneNumber: z.string().min(1, 'Phone number is required.'),
-    email: z.email({
-      error: (iss) => (iss.input === '' ? 'Email is required.' : undefined),
-    }),
-    password: z.string().transform((pwd) => pwd.trim()),
-    role: z.string().min(1, 'Role is required.'),
-    confirmPassword: z.string().transform((pwd) => pwd.trim()),
-    isEdit: z.boolean(),
-  })
-  .refine(
-    (data) => {
-      if (data.isEdit && !data.password) return true
-      return data.password.length > 0
-    },
-    {
-      message: 'Password is required.',
-      path: ['password'],
-    }
-  )
-  .refine(
-    ({ isEdit, password }) => {
-      if (isEdit && !password) return true
-      return password.length >= 8
-    },
-    {
-      message: 'Password must be at least 8 characters long.',
-      path: ['password'],
-    }
-  )
-  .refine(
-    ({ isEdit, password }) => {
-      if (isEdit && !password) return true
-      return /[a-z]/.test(password)
-    },
-    {
-      message: 'Password must contain at least one lowercase letter.',
-      path: ['password'],
-    }
-  )
-  .refine(
-    ({ isEdit, password }) => {
-      if (isEdit && !password) return true
-      return /\d/.test(password)
-    },
-    {
-      message: 'Password must contain at least one number.',
-      path: ['password'],
-    }
-  )
-  .refine(
-    ({ isEdit, password, confirmPassword }) => {
-      if (isEdit && !password) return true
-      return password === confirmPassword
+import { Textarea } from '@/components/ui/textarea'
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/components/ui/select'
+
+import { Button } from '@/components/ui/button'
+
+import { useCreateUser } from '@/hooks/users/useCreateUser'
+import { useUpdateUser } from '@/hooks/users/useUpdateUser'
+import { useUserDetail } from '@/hooks/users/useUserDetail'
+import { useProvinces } from '@/hooks/location/useProvinces'
+import { useCities } from '@/hooks/location/useCities'
+
+import {
+  genderOption,
+  footOptions,
+  positionOptions,
+  skillLevelOptions,
+  activityHistoryOptions,
+} from '@/constants/users/users-constants'
+
+import { useUsers } from './users-provider'
+import type { CreateUserPayload } from '@/types/users.types'
+import { useEffect } from 'react'
+
+export function UsersActionDialog() {
+  const { open, setOpen, currentRow } = useUsers()
+  const isAdd = open === 'add'
+  const isEdit = open === 'edit'
+  const isView = open === 'view'
+  const opened = isAdd || isEdit || isView
+  const readOnly = isView
+
+  const { data: user, isLoading } = useUserDetail(currentRow?.id as number)
+
+  const form = useForm<CreateUserPayload>({
+    defaultValues: {
+      username: '',
+      phone: '',
+      first_name: '',
+      last_name: '',
+      email: '',
+      province_id: undefined,
+      city_id: undefined,
+      gender: undefined,
+      birth_date: '',
+      height: undefined,
+      weight: undefined,
+      foot_specialization: '',
+      post_skill: '',
+      skill_level: '',
+      activity_history: false,
+      team_name: '',
+      favorite_iranian_team: '',
+      favorite_foreign_team: '',
+      shirt_number: undefined,
+      bio: '',
     },
-    {
-      message: "Passwords don't match.",
-      path: ['confirmPassword'],
+  })
+
+  useEffect(() => {
+    if (user && (isEdit || isView)) {
+      form.reset({
+        username: user.username ?? '',
+        phone: user.phone ?? '',
+        first_name: user.first_name ?? '',
+        last_name: user.last_name ?? '',
+        email: user.email ?? '',
+        province_id: user.province_id ?? undefined,
+        city_id: user.city_id ?? undefined,
+        gender: user.gender ?? undefined,
+        birth_date: user.birth_date ?? '',
+        height: user.height ?? undefined,
+        weight: user.weight ?? undefined,
+        foot_specialization: user.foot_specialization ?? '',
+        post_skill: user.post_skill ?? '',
+        skill_level: user.skill_level ?? '',
+        activity_history: Boolean(user.activity_history),
+        team_name: user.team_name ?? '',
+        favorite_iranian_team: user.favorite_iranian_team ?? '',
+        favorite_foreign_team: user.favorite_foreign_team ?? '',
+        shirt_number: user.shirt_number ?? undefined,
+        bio: user.bio ?? '',
+      })
+    } else if (isAdd) {
+      form.reset({
+        username: '',
+        phone: '',
+        first_name: '',
+        last_name: '',
+        email: '',
+        province_id: undefined,
+        city_id: undefined,
+        gender: undefined,
+        birth_date: '',
+        height: undefined,
+        weight: undefined,
+        foot_specialization: '',
+        post_skill: '',
+        skill_level: '',
+        activity_history: false,
+        team_name: '',
+        favorite_iranian_team: '',
+        favorite_foreign_team: '',
+        shirt_number: undefined,
+        bio: '',
+      })
     }
-  )
-type UserForm = z.infer<typeof formSchema>
+  }, [user, isEdit, isView, isAdd, form])
 
-type UserActionDialogProps = {
-  currentRow?: User
-  open: boolean
-  onOpenChange: (open: boolean) => void
-}
+  const provinceId = form.watch('province_id')
 
-export function UsersActionDialog({
-  currentRow,
-  open,
-  onOpenChange,
-}: UserActionDialogProps) {
-  const isEdit = !!currentRow
-  const form = useForm<UserForm>({
-    resolver: zodResolver(formSchema),
-    defaultValues: isEdit
-      ? {
-          ...currentRow,
-          password: '',
-          confirmPassword: '',
-          isEdit,
-        }
-      : {
-          firstName: '',
-          lastName: '',
-          username: '',
-          email: '',
-          role: '',
-          phoneNumber: '',
-          password: '',
-          confirmPassword: '',
-          isEdit,
-        },
-  })
+  const { data: provinces = [] } = useProvinces()
+  const { data: cities = [] } = useCities(provinceId as number)
 
-  const onSubmit = (values: UserForm) => {
-    form.reset()
-    showSubmittedData(values)
-    onOpenChange(false)
-  }
+  const createMutation = useCreateUser()
+  const updateMutation = useUpdateUser()
+
+  const onSubmit = (values: CreateUserPayload) => {
+    if (isView) return
+
+    if (isAdd) {
+      createMutation.mutate(values, {
+        onSuccess: () => setOpen(null),
+      })
+    }
 
-  const isPasswordTouched = !!form.formState.dirtyFields.password
+    if (isEdit && currentRow) {
+      updateMutation.mutate(
+        { id: currentRow.id, payload: values },
+        {
+          onSuccess: () => setOpen(null),
+        }
+      )
+    }
+  }
 
   return (
-    <Dialog
-      open={open}
-      onOpenChange={(state) => {
-        form.reset()
-        onOpenChange(state)
-      }}
-    >
-      <DialogContent className='sm:max-w-lg'>
-        <DialogHeader className='text-start'>
-          <DialogTitle>{isEdit ? 'Edit User' : 'Add New User'}</DialogTitle>
-          <DialogDescription>
-            {isEdit ? 'Update the user here. ' : 'Create new user here. '}
-            Click save when you&apos;re done.
-          </DialogDescription>
+    <Dialog open={opened} modal={true} onOpenChange={() => setOpen(null)}>
+      <DialogContent className='max-h-[90vh] max-w-5xl overflow-y-auto'>
+        <DialogHeader>
+          <DialogTitle>
+            {isAdd && 'افزودن کاربر'}
+            {isEdit && 'ویرایش کاربر'}
+            {isView && 'مشاهده کاربر'}
+          </DialogTitle>
         </DialogHeader>
-        <div className='h-105 w-[calc(100%+0.75rem)] overflow-y-auto py-1 pe-3'>
+
+        {isLoading ? (
+          <div className='py-10 text-center'>در حال دریافت اطلاعات...</div>
+        ) : (
           <Form {...form}>
-            <form
-              id='user-form'
-              onSubmit={form.handleSubmit(onSubmit)}
-              className='space-y-4 px-0.5'
-            >
-              <FormField
-                control={form.control}
-                name='firstName'
-                render={({ field }) => (
-                  <FormItem className='grid grid-cols-6 items-center space-y-0 gap-x-4 gap-y-1'>
-                    <FormLabel className='col-span-2 text-end'>
-                      First Name
-                    </FormLabel>
-                    <FormControl>
-                      <Input
-                        placeholder='John'
-                        className='col-span-4'
-                        autoComplete='off'
-                        {...field}
-                      />
-                    </FormControl>
-                    <FormMessage className='col-span-4 col-start-3' />
-                  </FormItem>
-                )}
-              />
-              <FormField
-                control={form.control}
-                name='lastName'
-                render={({ field }) => (
-                  <FormItem className='grid grid-cols-6 items-center space-y-0 gap-x-4 gap-y-1'>
-                    <FormLabel className='col-span-2 text-end'>
-                      Last Name
-                    </FormLabel>
-                    <FormControl>
-                      <Input
-                        placeholder='Doe'
-                        className='col-span-4'
-                        autoComplete='off'
-                        {...field}
-                      />
-                    </FormControl>
-                    <FormMessage className='col-span-4 col-start-3' />
-                  </FormItem>
-                )}
-              />
-              <FormField
-                control={form.control}
-                name='username'
-                render={({ field }) => (
-                  <FormItem className='grid grid-cols-6 items-center space-y-0 gap-x-4 gap-y-1'>
-                    <FormLabel className='col-span-2 text-end'>
-                      Username
-                    </FormLabel>
-                    <FormControl>
-                      <Input
-                        placeholder='john_doe'
-                        className='col-span-4'
-                        {...field}
-                      />
-                    </FormControl>
-                    <FormMessage className='col-span-4 col-start-3' />
-                  </FormItem>
-                )}
-              />
-              <FormField
-                control={form.control}
-                name='email'
-                render={({ field }) => (
-                  <FormItem className='grid grid-cols-6 items-center space-y-0 gap-x-4 gap-y-1'>
-                    <FormLabel className='col-span-2 text-end'>Email</FormLabel>
-                    <FormControl>
-                      <Input
-                        placeholder='john.doe@gmail.com'
-                        className='col-span-4'
-                        {...field}
-                      />
-                    </FormControl>
-                    <FormMessage className='col-span-4 col-start-3' />
-                  </FormItem>
-                )}
-              />
-              <FormField
-                control={form.control}
-                name='phoneNumber'
-                render={({ field }) => (
-                  <FormItem className='grid grid-cols-6 items-center space-y-0 gap-x-4 gap-y-1'>
-                    <FormLabel className='col-span-2 text-end'>
-                      Phone Number
-                    </FormLabel>
-                    <FormControl>
-                      <Input
-                        placeholder='+123456789'
-                        className='col-span-4'
-                        {...field}
-                      />
-                    </FormControl>
-                    <FormMessage className='col-span-4 col-start-3' />
-                  </FormItem>
-                )}
-              />
-              <FormField
-                control={form.control}
-                name='role'
-                render={({ field }) => (
-                  <FormItem className='grid grid-cols-6 items-center space-y-0 gap-x-4 gap-y-1'>
-                    <FormLabel className='col-span-2 text-end'>Role</FormLabel>
-                    <SelectDropdown
-                      defaultValue={field.value}
-                      onValueChange={field.onChange}
-                      placeholder='Select a role'
-                      className='col-span-4'
-                      items={roles.map(({ label, value }) => ({
-                        label,
-                        value,
-                      }))}
-                    />
-                    <FormMessage className='col-span-4 col-start-3' />
-                  </FormItem>
-                )}
-              />
-              <FormField
-                control={form.control}
-                name='password'
-                render={({ field }) => (
-                  <FormItem className='grid grid-cols-6 items-center space-y-0 gap-x-4 gap-y-1'>
-                    <FormLabel className='col-span-2 text-end'>
-                      Password
-                    </FormLabel>
-                    <FormControl>
-                      <PasswordInput
-                        placeholder='e.g., S3cur3P@ssw0rd'
-                        className='col-span-4'
-                        {...field}
-                      />
-                    </FormControl>
-                    <FormMessage className='col-span-4 col-start-3' />
-                  </FormItem>
-                )}
-              />
+            <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
+              <div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
+                <FormField
+                  control={form.control}
+                  name='first_name'
+                  rules={{ required: 'نام الزامی است' }}
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>نام</FormLabel>
+                      <FormControl>
+                        <Input {...field} disabled={readOnly} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='last_name'
+                  rules={{ required: 'نام خانوادگی الزامی است' }}
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>نام خانوادگی</FormLabel>
+                      <FormControl>
+                        <Input {...field} disabled={readOnly} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='username'
+                  rules={{ required: 'نام کاربری الزامی است' }}
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>نام کاربری</FormLabel>
+                      <FormControl>
+                        <Input {...field} disabled={readOnly} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='phone'
+                  rules={{
+                    required: 'شماره موبایل الزامی است',
+                    minLength: {
+                      value: 10,
+                      message: 'شماره موبایل نامعتبر است',
+                    },
+                  }}
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>موبایل</FormLabel>
+                      <FormControl>
+                        <Input {...field} disabled={readOnly} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='email'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>ایمیل</FormLabel>
+                      <FormControl>
+                        <Input
+                          {...field}
+                          disabled={readOnly}
+                          value={field.value ?? ''}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='birth_date'
+                  rules={{ required: 'تاریخ تولد الزامی است' }}
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>تاریخ تولد</FormLabel>
+                      <FormControl>
+                        <Input type='date' {...field} disabled={readOnly} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='gender'
+                  rules={{ required: 'جنسیت الزامی است' }}
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>جنسیت</FormLabel>
+
+                      <Select
+                        disabled={readOnly}
+                        value={field.value?.toString() ?? ''}
+                        onValueChange={(value) => field.onChange(Number(value))}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder='انتخاب جنسیت' />
+                          </SelectTrigger>
+                        </FormControl>
+
+                        <SelectContent>
+                          {genderOption.map((item) => (
+                            <SelectItem
+                              key={item.value}
+                              value={String(item.value)}
+                            >
+                              {item.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='province_id'
+                  rules={{ required: 'انتخاب استان الزامی است' }}
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>استان</FormLabel>
+                      <Select
+                        disabled={readOnly}
+                        value={field.value ? field.value.toString() : ''}
+                        onValueChange={(value) => {
+                          field.onChange(value ? Number(value) : undefined)
+                          form.setValue('city_id', undefined)
+                        }}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder='انتخاب استان' />
+                          </SelectTrigger>
+                        </FormControl>
+
+                        <SelectContent>
+                          {provinces.map((province) => (
+                            <SelectItem
+                              key={province.id}
+                              value={String(province.id)}
+                            >
+                              {province.name}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='city_id'
+                  rules={{ required: 'انتخاب شهر الزامی است' }}
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>شهر</FormLabel>
+
+                      <Select
+                        disabled={readOnly || !provinceId}
+                        value={field.value ? String(field.value) : ''}
+                        onValueChange={(value) => field.onChange(Number(value))}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder='ابتدا استان را انتخاب کنید' />
+                          </SelectTrigger>
+                        </FormControl>
+
+                        <SelectContent>
+                          {cities.map((city) => (
+                            <SelectItem key={city.id} value={String(city.id)}>
+                              {city.name}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='height'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>قد</FormLabel>
+
+                      <FormControl>
+                        <Input
+                          type='number'
+                          value={field.value ?? ''}
+                          disabled={readOnly}
+                          onChange={(e) =>
+                            field.onChange(
+                              e.target.value === ''
+                                ? undefined
+                                : Number(e.target.value)
+                            )
+                          }
+                        />
+                      </FormControl>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='weight'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>وزن</FormLabel>
+
+                      <FormControl>
+                        <Input
+                          type='number'
+                          value={field.value ?? ''}
+                          disabled={readOnly}
+                          onChange={(e) =>
+                            field.onChange(
+                              e.target.value === ''
+                                ? undefined
+                                : Number(e.target.value)
+                            )
+                          }
+                        />
+                      </FormControl>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='foot_specialization'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>پای تخصصی</FormLabel>
+
+                      <Select
+                        disabled={readOnly}
+                        value={field.value ?? ''}
+                        onValueChange={field.onChange}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder='انتخاب کنید' />
+                          </SelectTrigger>
+                        </FormControl>
+
+                        <SelectContent>
+                          {footOptions.map((item) => (
+                            <SelectItem key={item.value} value={item.value}>
+                              {item.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='post_skill'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>پست تخصصی</FormLabel>
+
+                      <Select
+                        disabled={readOnly}
+                        value={field.value ?? ''}
+                        onValueChange={field.onChange}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder='انتخاب کنید' />
+                          </SelectTrigger>
+                        </FormControl>
+
+                        <SelectContent>
+                          {positionOptions.map((item) => (
+                            <SelectItem key={item.value} value={item.value}>
+                              {item.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='skill_level'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>سطح مهارت</FormLabel>
+
+                      <Select
+                        disabled={readOnly}
+                        value={field.value ?? ''}
+                        onValueChange={field.onChange}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder='انتخاب کنید' />
+                          </SelectTrigger>
+                        </FormControl>
+
+                        <SelectContent>
+                          {skillLevelOptions.map((item) => (
+                            <SelectItem key={item.value} value={item.value}>
+                              {item.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={form.control}
+                  name='activity_history'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>سابقه فعالیت</FormLabel>
+
+                      <Select
+                        disabled={readOnly}
+                        value={String(field.value)}
+                        onValueChange={(v) => field.onChange(v === 'true')}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder='انتخاب کنید' />
+                          </SelectTrigger>
+                        </FormControl>
+
+                        <SelectContent>
+                          {activityHistoryOptions.map((item) => (
+                            <SelectItem
+                              key={String(item.value)}
+                              value={String(item.value)}
+                            >
+                              {item.label}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+              </div>
+
               <FormField
                 control={form.control}
-                name='confirmPassword'
+                name='bio'
                 render={({ field }) => (
-                  <FormItem className='grid grid-cols-6 items-center space-y-0 gap-x-4 gap-y-1'>
-                    <FormLabel className='col-span-2 text-end'>
-                      Confirm Password
-                    </FormLabel>
+                  <FormItem>
+                    <FormLabel>بیوگرافی</FormLabel>
+
                     <FormControl>
-                      <PasswordInput
-                        disabled={!isPasswordTouched}
-                        placeholder='e.g., S3cur3P@ssw0rd'
-                        className='col-span-4'
+                      <Textarea
                         {...field}
+                        disabled={readOnly}
+                        value={field.value ?? ''}
                       />
                     </FormControl>
-                    <FormMessage className='col-span-4 col-start-3' />
+
+                    <FormMessage />
                   </FormItem>
                 )}
               />
+
+              {!readOnly && (
+                <div className='flex justify-end gap-2'>
+                  <Button
+                    type='button'
+                    variant='outline'
+                    onClick={() => setOpen(null)}
+                  >
+                    انصراف
+                  </Button>
+
+                  <Button
+                    type='submit'
+                    disabled={
+                      createMutation.isPending || updateMutation.isPending
+                    }
+                  >
+                    {isAdd ? 'ایجاد کاربر' : 'ذخیره تغییرات'}
+                  </Button>
+                </div>
+              )}
             </form>
           </Form>
-        </div>
-        <DialogFooter>
-          <Button type='submit' form='user-form'>
-            Save changes
-          </Button>
-        </DialogFooter>
+        )}
       </DialogContent>
     </Dialog>
   )

+ 55 - 62
src/features/users/components/users-columns.tsx

@@ -1,11 +1,15 @@
 import { type ColumnDef } from '@tanstack/react-table'
+
 import { cn } from '@/lib/utils'
+
 import { Badge } from '@/components/ui/badge'
 import { Checkbox } from '@/components/ui/checkbox'
+
 import { DataTableColumnHeader } from '@/components/data-table'
 import { LongText } from '@/components/long-text'
-import { callTypes, roles } from '../data/data'
-import { type User } from '../data/schema'
+
+import { type User } from '@/types/users.types'
+
 import { DataTableRowActions } from './data-table-row-actions'
 
 export const usersColumns: ColumnDef<User>[] = [
@@ -22,9 +26,6 @@ export const usersColumns: ColumnDef<User>[] = [
         className='translate-y-0.5'
       />
     ),
-    meta: {
-      className: cn('inset-s-0 z-10 rounded-tl-[inherit] max-md:sticky'),
-    },
     cell: ({ row }) => (
       <Checkbox
         checked={row.getIsSelected()}
@@ -35,104 +36,96 @@ export const usersColumns: ColumnDef<User>[] = [
     ),
     enableSorting: false,
     enableHiding: false,
+    meta: {
+      className: cn('inset-s-0 z-10 rounded-tl-[inherit] max-md:sticky'),
+    },
   },
+
+  {
+    accessorKey: 'id',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='شناسه' />
+    ),
+    cell: ({ row }) => <div className='w-15'>#{row.original.id}</div>,
+    enableSorting: true,
+  },
+
   {
     accessorKey: 'username',
     header: ({ column }) => (
       <DataTableColumnHeader column={column} title='نام کاربری' />
     ),
     cell: ({ row }) => (
-      <LongText className='max-w-36 ps-3'>{row.getValue('username')}</LongText>
+      <LongText className='max-w-36 ps-3'>{row.original.username}</LongText>
     ),
+    enableHiding: false,
     meta: {
       className: cn(
-        'drop-shadow-[0_1px_2px_rgb(0_0_0_/_0.1)] dark:drop-shadow-[0_1px_2px_rgb(255_255_255_/_0.1)]',
-        'inset-s-6 ps-0.5 max-md:sticky @4xl/content:table-cell @4xl/content:drop-shadow-none'
+        'drop-shadow-[0_1px_2px_rgb(0_0_0_/_0.1)]',
+        'dark:drop-shadow-[0_1px_2px_rgb(255_255_255_/_0.1)]',
+        'inset-s-6 ps-0.5 max-md:sticky',
+        '@4xl/content:table-cell',
+        '@4xl/content:drop-shadow-none'
       ),
     },
-    enableHiding: false,
   },
+
   {
-    id: 'fullName',
+    accessorKey: 'full_name',
     header: ({ column }) => (
-      <DataTableColumnHeader column={column} title='نام' />
+      <DataTableColumnHeader column={column} title='نام کامل' />
+    ),
+    cell: ({ row }) => (
+      <LongText className='max-w-40'>{row.original.full_name}</LongText>
     ),
-    cell: ({ row }) => {
-      const { first_name, last_name } = row.original
-      const fullName = `${first_name} ${last_name}`
-      return <LongText className='max-w-36'>{fullName}</LongText>
-    },
-    meta: { className: 'w-36' },
   },
+
+  {
+    accessorKey: 'phone',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='شماره موبایل' />
+    ),
+    cell: ({ row }) => <div dir='ltr'>{row.original.phone}</div>,
+  },
+
   {
     accessorKey: 'email',
     header: ({ column }) => (
       <DataTableColumnHeader column={column} title='ایمیل' />
     ),
     cell: ({ row }) => (
-      <div className='w-fit ps-2 text-nowrap'>{row.getValue('email')}</div>
+      <div className='text-nowrap'>{row.original.email ?? '-'}</div>
     ),
   },
+
   {
-    accessorKey: 'phone',
+    accessorKey: 'followers_count',
     header: ({ column }) => (
-      <DataTableColumnHeader column={column} title='شماره موبایل' />
+      <DataTableColumnHeader column={column} title='دنبال‌کننده' />
     ),
-    cell: ({ row }) => <div>{row.getValue('phone')}</div>,
-    enableSorting: false,
-  },
-  {
-    accessorKey: 'status',
-    header: ({ column }) => (
-      <DataTableColumnHeader column={column} title='وضعیت' />
+    cell: ({ row }) => (
+      <Badge variant='secondary'>{row.original.followers_count}</Badge>
     ),
-    cell: ({ row }) => {
-      const { status } = row.original
-      const badgeColor = callTypes.get(status)
-      return (
-        <div className='flex space-x-2'>
-          <Badge variant='outline' className={cn('capitalize', badgeColor)}>
-            {row.getValue('status')}
-          </Badge>
-        </div>
-      )
-    },
-    filterFn: (row, id, value) => {
-      return value.includes(row.getValue(id))
-    },
-    enableHiding: false,
-    enableSorting: false,
   },
+
   {
-    accessorKey: 'role',
+    accessorKey: 'created_at',
     header: ({ column }) => (
-      <DataTableColumnHeader column={column} title='نقش' />
+      <DataTableColumnHeader column={column} title='تاریخ عضویت' />
     ),
     cell: ({ row }) => {
-      const { role } = row.original
-      const userType = roles.find(({ value }) => value === role)
-
-      if (!userType) {
-        return null
-      }
-
       return (
-        <div className='flex items-center gap-x-2'>
-          {userType.icon && (
-            <userType.icon size={16} className='text-muted-foreground' />
-          )}
-          <span className='text-sm capitalize'>{row.getValue('role')}</span>
+        <div className='text-sm text-muted-foreground'>
+          {new Date(row.original.created_at).toLocaleDateString('fa-IR')}
         </div>
       )
     },
-    filterFn: (row, id, value) => {
-      return value.includes(row.getValue(id))
-    },
-    enableSorting: false,
-    enableHiding: false,
   },
+
   {
     id: 'actions',
     cell: DataTableRowActions,
+    enableSorting: false,
+    enableHiding: false,
   },
 ]

+ 0 - 178
src/features/users/components/users-delete-dialog.test.tsx

@@ -1,178 +0,0 @@
-import { useState } from 'react'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { showSubmittedData } from '@/lib/show-submitted-data'
-import { type User } from '../data/schema'
-import { UsersDeleteDialog } from './users-delete-dialog'
-
-vi.mock('@/lib/show-submitted-data', () => ({ showSubmittedData: vi.fn() }))
-
-const MOCK_USER: User = {
-  id: 'user-delete-test',
-  firstName: 'John',
-  lastName: 'Doe',
-  username: 'john_doe',
-  email: 'johndoe@shadcn-admin.com',
-  phoneNumber: '+959123456789',
-  status: 'active',
-  role: 'manager',
-  createdAt: new Date('2026-01-01'),
-  updatedAt: new Date('2026-02-02'),
-}
-
-describe('UsersDeleteDialog', () => {
-  beforeEach(() => vi.clearAllMocks())
-
-  it('renders the dialog with the correct title, description, input and buttons', async () => {
-    const { getByText, getByRole } = await render(
-      <UsersDeleteDialog open onOpenChange={vi.fn()} currentRow={MOCK_USER} />
-    )
-
-    const title = getByRole('heading', {
-      level: 2,
-      name: /Delete User/i,
-    })
-    const desc = getByText(
-      new RegExp(`Are you sure you want to delete ${MOCK_USER.username}?`, 'i')
-    )
-    const usernameInput = getByRole('textbox', { name: /Username/i })
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(title).toBeInTheDocument()
-    await expect.element(desc).toBeInTheDocument()
-    await expect.element(usernameInput).toBeInTheDocument()
-    await expect.element(cancelButton).toBeInTheDocument()
-    await expect.element(deleteButton).toBeInTheDocument()
-    await expect.element(deleteButton).toBeDisabled()
-  })
-
-  it('keeps the delete button disabled until the username input is filled correctly', async () => {
-    const { getByRole } = await render(
-      <UsersDeleteDialog open onOpenChange={vi.fn()} currentRow={MOCK_USER} />
-    )
-
-    const usernameInput = getByRole('textbox', { name: /Username/i })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(usernameInput, 'wrong-username')
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(usernameInput, MOCK_USER.username)
-    await expect.element(deleteButton).toBeEnabled()
-  })
-
-  it('closes the dialog when the cancel button is clicked', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersDeleteDialog
-        open
-        onOpenChange={onOpenChange}
-        currentRow={MOCK_USER}
-      />
-    )
-
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    await userEvent.click(cancelButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-  })
-
-  it('resets the username input when the dialog is closed and reopened', async () => {
-    function Harness() {
-      const [open, setOpen] = useState(true)
-      return (
-        <>
-          <button type='button' onClick={() => setOpen(true)}>
-            Reopen
-          </button>
-          {open ? (
-            <UsersDeleteDialog
-              open={open}
-              onOpenChange={setOpen}
-              currentRow={MOCK_USER}
-            />
-          ) : null}
-        </>
-      )
-    }
-
-    const { getByRole } = await render(<Harness />)
-
-    const usernameInput = getByRole('textbox', { name: /Username/i })
-    await userEvent.fill(usernameInput, MOCK_USER.username)
-    await expect.element(usernameInput).toHaveValue(MOCK_USER.username)
-
-    const closeButton = getByRole('button', { name: /Cancel/i })
-    await userEvent.click(closeButton)
-
-    const reopenButton = getByRole('button', { name: /Reopen/i })
-    await userEvent.click(reopenButton)
-    await expect.element(usernameInput).toHaveValue('')
-  })
-
-  it('shows the submitted data when deleted successfully', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersDeleteDialog
-        open
-        onOpenChange={onOpenChange}
-        currentRow={MOCK_USER}
-      />
-    )
-
-    const usernameInput = getByRole('textbox', { name: /Username/i })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(usernameInput, MOCK_USER.username)
-
-    await expect.element(deleteButton).toBeEnabled()
-
-    await userEvent.click(deleteButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-
-    expect(showSubmittedData).toHaveBeenCalledOnce()
-    expect(showSubmittedData).toHaveBeenCalledWith(
-      MOCK_USER,
-      'The following user has been deleted:'
-    )
-  })
-
-  it('deletes successfully when press Enter key on the username input', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersDeleteDialog
-        open
-        onOpenChange={onOpenChange}
-        currentRow={MOCK_USER}
-      />
-    )
-
-    const usernameInput = getByRole('textbox', { name: /Username/i })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(usernameInput, MOCK_USER.username)
-    await expect.element(deleteButton).toBeEnabled()
-
-    await userEvent.keyboard('{Enter}')
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-
-    expect(showSubmittedData).toHaveBeenCalledOnce()
-    expect(showSubmittedData).toHaveBeenCalledWith(
-      MOCK_USER,
-      'The following user has been deleted:'
-    )
-  })
-})

+ 16 - 17
src/features/users/components/users-delete-dialog.tsx

@@ -1,32 +1,30 @@
-'use client'
-
 import { useState } from 'react'
 import { AlertTriangle } from 'lucide-react'
-import { showSubmittedData } from '@/lib/show-submitted-data'
 import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { ConfirmDialog } from '@/components/confirm-dialog'
-import { type User } from '../data/schema'
+import { useDeleteUser } from '@/hooks/users/useDeleteUser'
+import { type User } from '@/types/users.types'
 
-type UserDeleteDialogProps = {
+interface Props {
   open: boolean
   onOpenChange: (open: boolean) => void
-  currentRow: User
+  currentRow: User | null
 }
 
-export function UsersDeleteDialog({
-  open,
-  onOpenChange,
-  currentRow,
-}: UserDeleteDialogProps) {
+export function UsersDeleteDialog({ open, onOpenChange, currentRow }: Props) {
   const [value, setValue] = useState('')
+  const { mutate, isPending } = useDeleteUser()
 
-  const handleDelete = () => {
-    if (value.trim() !== currentRow.username) return
+  if (!currentRow) return null
 
-    onOpenChange(false)
-    showSubmittedData(currentRow, 'The following user has been deleted:')
+  const handleDelete = () => {
+    mutate(currentRow.id, {
+      onSuccess: () => {
+        onOpenChange(false)
+      },
+    })
   }
 
   return (
@@ -55,13 +53,13 @@ export function UsersDeleteDialog({
         >
           <p className='mb-2'>
             آیا مطمئن هستید که می خواهید{' '}
-            <span className='font-bold'>{currentRow.username}</span> حذف کنید ؟
+            <span className='font-bold'>{currentRow.full_name}</span> حذف کنید ؟
             <br />
             این عمل برای همیشه کاربر را با نقش حذف می کند{' '}
             {/* <span className='font-bold'>
               {currentRow.role.toUpperCase() ?? ''}
             </span>{' '} */}
-            از سیستم این قابل واگرد نیست.
+            {/* از سیستم این قابل واگرد نیست. */}
           </p>
 
           <Label>
@@ -84,6 +82,7 @@ export function UsersDeleteDialog({
       }
       confirmText='حذف'
       cancelBtnText='انصراف'
+      isLoading={isPending}
       destructive
     />
   )

+ 30 - 38
src/features/users/components/users-dialogs.tsx

@@ -1,51 +1,43 @@
-import { UsersActionDialog } from './users-action-dialog'
-import { UsersDeleteDialog } from './users-delete-dialog'
-import { UsersInviteDialog } from './users-invite-dialog'
 import { useUsers } from './users-provider'
 
+// import { UsersInviteDialog } from './users-invite-dialog'
+import { UsersDeleteDialog } from './users-delete-dialog'
+import { UsersActionDialog } from './users-action-dialog'
+// import { UsersMultiDeleteDialog } from './users-multi-delete-dialog'
+
 export function UsersDialogs() {
   const { open, setOpen, currentRow, setCurrentRow } = useUsers()
+
+  const handleClose = () => {
+    setOpen(null)
+    setCurrentRow(null)
+  }
+
   return (
     <>
-      <UsersActionDialog
-        key='user-add'
-        open={open === 'add'}
-        onOpenChange={() => setOpen('add')}
-      />
+      <UsersActionDialog />
 
-      <UsersInviteDialog
-        key='user-invite'
+      {/* <UsersInviteDialog
         open={open === 'invite'}
-        onOpenChange={() => setOpen('invite')}
-      />
+        onOpenChange={(state) => {
+          if (!state) handleClose()
+        }}
+      /> */}
 
-      {currentRow && (
-        <>
-          <UsersActionDialog
-            key={`user-edit-${currentRow.id}`}
-            open={open === 'edit'}
-            onOpenChange={() => {
-              setOpen('edit')
-              setTimeout(() => {
-                setCurrentRow(null)
-              }, 500)
-            }}
-            currentRow={currentRow}
-          />
+      <UsersDeleteDialog
+        open={open === 'delete'}
+        onOpenChange={(state) => {
+          if (!state) handleClose()
+        }}
+        currentRow={currentRow}
+      />
 
-          <UsersDeleteDialog
-            key={`user-delete-${currentRow.id}`}
-            open={open === 'delete'}
-            onOpenChange={() => {
-              setOpen('delete')
-              setTimeout(() => {
-                setCurrentRow(null)
-              }, 500)
-            }}
-            currentRow={currentRow}
-          />
-        </>
-      )}
+      {/* <UsersMultiDeleteDialog
+        open={open === 'multi-delete'}
+        onOpenChange={(state) => {
+          if (!state) handleClose()
+        }}
+      /> */}
     </>
   )
 }

+ 0 - 157
src/features/users/components/users-invite-dialog.test.tsx

@@ -1,157 +0,0 @@
-import { useState } from 'react'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { showSubmittedData } from '@/lib/show-submitted-data'
-import { UsersInviteDialog } from './users-invite-dialog'
-
-vi.mock('@/lib/show-submitted-data', () => ({ showSubmittedData: vi.fn() }))
-
-describe('UsersInviteDialog', () => {
-  beforeEach(() => vi.clearAllMocks())
-
-  it('renders the dialog title and description', async () => {
-    const { getByRole, getByText } = await render(
-      <UsersInviteDialog open onOpenChange={vi.fn()} />
-    )
-
-    const title = getByRole('heading', {
-      level: 2,
-      name: /Invite User/i,
-    })
-    const desc = getByText(/Invite new user to join your team/i)
-
-    await expect.element(title).toBeInTheDocument()
-    await expect.element(desc).toBeInTheDocument()
-  })
-
-  it('closes when the dialog close button is clicked', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersInviteDialog open onOpenChange={onOpenChange} />
-    )
-
-    const closeButton = getByRole('button', { name: /Close/i })
-    await userEvent.click(closeButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-  })
-
-  it('closes when Cancel is clicked', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersInviteDialog open onOpenChange={onOpenChange} />
-    )
-
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    await userEvent.click(cancelButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-  })
-
-  it('shows error messages when submitting empty form, and clears them as fields are filled', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole, getByText } = await render(
-      <UsersInviteDialog open onOpenChange={onOpenChange} />
-    )
-
-    const emailErrorMessage = getByText(/Please enter an email to invite./i)
-    const roleErrorMessage = getByText(/Role is required./i)
-
-    const submitButton = getByRole('button', { name: /Invite/i })
-    await userEvent.click(submitButton)
-
-    await expect.element(emailErrorMessage).toBeInTheDocument()
-    await expect.element(roleErrorMessage).toBeInTheDocument()
-
-    const emailInput = getByRole('textbox', { name: /Email/i })
-    await userEvent.fill(emailInput, 'test@example.com')
-
-    const roleSelect = getByRole('combobox', { name: /Role/i })
-    await userEvent.click(roleSelect)
-    await userEvent.click(getByRole('option', { name: /Superadmin/i }))
-
-    await expect.element(emailErrorMessage).not.toBeInTheDocument()
-    await expect.element(roleErrorMessage).not.toBeInTheDocument()
-  })
-
-  it('resets entered values when the dialog is closed and reopened', async () => {
-    function Harness() {
-      const [open, setOpen] = useState(true)
-      return (
-        <>
-          <button type='button' onClick={() => setOpen(true)}>
-            Reopen
-          </button>
-          <UsersInviteDialog open={open} onOpenChange={setOpen} />
-        </>
-      )
-    }
-
-    const { getByRole } = await render(<Harness />)
-
-    const EMAIL_VALUE = 'test@example.com'
-    const ROLE_VALUE = 'Superadmin'
-    const DESC_VALUE = 'This is a test description'
-
-    const emailInput = getByRole('textbox', { name: /Email/i })
-    await userEvent.fill(emailInput, EMAIL_VALUE)
-
-    const roleSelect = getByRole('combobox', { name: /Role/i })
-    await userEvent.click(roleSelect)
-    await userEvent.click(getByRole('option', { name: ROLE_VALUE }))
-
-    const descInput = getByRole('textbox', { name: /Description/i })
-    await userEvent.fill(descInput, DESC_VALUE)
-
-    await expect.element(emailInput).toHaveValue(EMAIL_VALUE)
-    await expect.element(roleSelect).toHaveTextContent(ROLE_VALUE)
-    await expect.element(descInput).toHaveValue(DESC_VALUE)
-
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    await userEvent.click(cancelButton)
-
-    const reopenButton = getByRole('button', { name: /Reopen/i })
-    await userEvent.click(reopenButton)
-
-    await expect.element(emailInput).toHaveValue('')
-    await expect.element(roleSelect).toHaveTextContent('Select a role')
-    await expect.element(descInput).toHaveValue('')
-  })
-
-  it('shows submitted data when the form is submitted successfully', async () => {
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersInviteDialog open onOpenChange={onOpenChange} />
-    )
-
-    const EMAIL_VALUE = 'test@example.com'
-    const ROLE_VALUE = 'superadmin'
-    const DESC_VALUE = 'Welcome aboard!'
-
-    const emailInput = getByRole('textbox', { name: /Email/i })
-    await userEvent.fill(emailInput, EMAIL_VALUE)
-
-    const roleSelect = getByRole('combobox', { name: /Role/i })
-    await userEvent.click(roleSelect)
-    await userEvent.click(getByRole('option', { name: ROLE_VALUE }))
-
-    const descInput = getByRole('textbox', { name: /Description/i })
-    await userEvent.fill(descInput, DESC_VALUE)
-
-    const submitButton = getByRole('button', { name: /Invite/i })
-    await userEvent.click(submitButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-
-    expect(showSubmittedData).toHaveBeenCalledOnce()
-    expect(showSubmittedData).toHaveBeenCalledWith({
-      email: EMAIL_VALUE,
-      role: ROLE_VALUE,
-      desc: DESC_VALUE,
-    })
-  })
-})

+ 0 - 150
src/features/users/components/users-invite-dialog.tsx

@@ -1,150 +0,0 @@
-import { z } from 'zod'
-import { useForm } from 'react-hook-form'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { MailPlus, Send } from 'lucide-react'
-import { showSubmittedData } from '@/lib/show-submitted-data'
-import { Button } from '@/components/ui/button'
-import {
-  Dialog,
-  DialogClose,
-  DialogContent,
-  DialogDescription,
-  DialogFooter,
-  DialogHeader,
-  DialogTitle,
-} from '@/components/ui/dialog'
-import {
-  Form,
-  FormControl,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from '@/components/ui/form'
-import { Input } from '@/components/ui/input'
-import { Textarea } from '@/components/ui/textarea'
-import { SelectDropdown } from '@/components/select-dropdown'
-import { roles } from '../data/data'
-
-const formSchema = z.object({
-  email: z.email({
-    error: (iss) =>
-      iss.input === '' ? 'Please enter an email to invite.' : undefined,
-  }),
-  role: z.string().min(1, 'Role is required.'),
-  desc: z.string().optional(),
-})
-
-type UserInviteForm = z.infer<typeof formSchema>
-
-type UserInviteDialogProps = {
-  open: boolean
-  onOpenChange: (open: boolean) => void
-}
-
-export function UsersInviteDialog({
-  open,
-  onOpenChange,
-}: UserInviteDialogProps) {
-  const form = useForm<UserInviteForm>({
-    resolver: zodResolver(formSchema),
-    defaultValues: { email: '', role: '', desc: '' },
-  })
-
-  const onSubmit = (values: UserInviteForm) => {
-    form.reset()
-    showSubmittedData(values)
-    onOpenChange(false)
-  }
-
-  return (
-    <Dialog
-      open={open}
-      onOpenChange={(state) => {
-        form.reset()
-        onOpenChange(state)
-      }}
-    >
-      <DialogContent className='sm:max-w-md'>
-        <DialogHeader className='text-start'>
-          <DialogTitle className='flex items-center gap-2'>
-            <MailPlus /> Invite User
-          </DialogTitle>
-          <DialogDescription>
-            Invite new user to join your team by sending them an email
-            invitation. Assign a role to define their access level.
-          </DialogDescription>
-        </DialogHeader>
-        <Form {...form}>
-          <form
-            id='user-invite-form'
-            onSubmit={form.handleSubmit(onSubmit)}
-            className='space-y-4'
-          >
-            <FormField
-              control={form.control}
-              name='email'
-              render={({ field }) => (
-                <FormItem>
-                  <FormLabel>Email</FormLabel>
-                  <FormControl>
-                    <Input
-                      type='email'
-                      placeholder='eg: john.doe@gmail.com'
-                      {...field}
-                    />
-                  </FormControl>
-                  <FormMessage />
-                </FormItem>
-              )}
-            />
-            <FormField
-              control={form.control}
-              name='role'
-              render={({ field }) => (
-                <FormItem>
-                  <FormLabel>Role</FormLabel>
-                  <SelectDropdown
-                    defaultValue={field.value}
-                    onValueChange={field.onChange}
-                    placeholder='Select a role'
-                    items={roles.map(({ label, value }) => ({
-                      label,
-                      value,
-                    }))}
-                  />
-                  <FormMessage />
-                </FormItem>
-              )}
-            />
-            <FormField
-              control={form.control}
-              name='desc'
-              render={({ field }) => (
-                <FormItem className=''>
-                  <FormLabel>Description (optional)</FormLabel>
-                  <FormControl>
-                    <Textarea
-                      className='resize-none'
-                      placeholder='Add a personal note to your invitation (optional)'
-                      {...field}
-                    />
-                  </FormControl>
-                  <FormMessage />
-                </FormItem>
-              )}
-            />
-          </form>
-        </Form>
-        <DialogFooter className='gap-y-2'>
-          <DialogClose asChild>
-            <Button variant='outline'>Cancel</Button>
-          </DialogClose>
-          <Button type='submit' form='user-invite-form'>
-            Invite <Send />
-          </Button>
-        </DialogFooter>
-      </DialogContent>
-    </Dialog>
-  )
-}

+ 0 - 161
src/features/users/components/users-multi-delete-dialog.test.tsx

@@ -1,161 +0,0 @@
-import { useState } from 'react'
-import { createTableMock } from '@/test-utils/tanstack-table'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render } from 'vitest-browser-react'
-import { userEvent } from 'vitest/browser'
-import { UsersMultiDeleteDialog } from './users-multi-delete-dialog'
-
-vi.mock('@/lib/utils', async (orig) => ({
-  ...(await orig()),
-  sleep: vi.fn(() => Promise.resolve()),
-}))
-
-describe('UsersMultiDeleteDialog', () => {
-  beforeEach(() => vi.clearAllMocks())
-
-  it('renders the dialog with the correct title, description, input and buttons', async () => {
-    const { table } = createTableMock()
-
-    const { getByRole, getByText } = await render(
-      <UsersMultiDeleteDialog open onOpenChange={vi.fn()} table={table} />
-    )
-
-    const title = getByRole('heading', {
-      level: 2,
-      name: /Delete 2 users/i,
-    })
-    const desc = getByText(
-      new RegExp(`Are you sure you want to delete the selected users?`, 'i')
-    )
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(title).toBeInTheDocument()
-    await expect.element(desc).toBeInTheDocument()
-    await expect.element(confirmDeleteInput).toBeInTheDocument()
-    await expect.element(deleteButton).toBeInTheDocument()
-    await expect.element(deleteButton).toBeDisabled()
-  })
-
-  it('keeps the delete button disabled until the confirm delete input is filled correctly', async () => {
-    const { table } = createTableMock()
-    const { getByRole } = await render(
-      <UsersMultiDeleteDialog open onOpenChange={vi.fn()} table={table} />
-    )
-
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(confirmDeleteInput, 'wrong-input')
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(confirmDeleteInput, 'DELETE')
-    await expect.element(deleteButton).toBeEnabled()
-  })
-
-  it('closes the dialog when the cancel button is clicked', async () => {
-    const { table } = createTableMock()
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersMultiDeleteDialog open onOpenChange={onOpenChange} table={table} />
-    )
-
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    await userEvent.click(cancelButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-  })
-
-  it('resets the confirm delete input when the dialog is closed and reopened', async () => {
-    const { table } = createTableMock()
-
-    function Harness() {
-      const [open, setOpen] = useState(true)
-      return (
-        <>
-          <button type='button' onClick={() => setOpen(true)}>
-            Reopen
-          </button>
-          {open ? (
-            <UsersMultiDeleteDialog
-              open={open}
-              onOpenChange={setOpen}
-              table={table}
-            />
-          ) : null}
-        </>
-      )
-    }
-
-    const { getByRole } = await render(<Harness />)
-
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    await userEvent.fill(confirmDeleteInput, 'DELETE')
-    await expect.element(confirmDeleteInput).toHaveValue('DELETE')
-
-    const cancelButton = getByRole('button', { name: /Cancel/i })
-    await userEvent.click(cancelButton)
-
-    const reopenButton = getByRole('button', { name: /Reopen/i })
-    await userEvent.click(reopenButton)
-    await expect.element(confirmDeleteInput).toHaveValue('')
-  })
-
-  it('shows the submitted data when deleted successfully', async () => {
-    const { table, resetRowSelection } = createTableMock()
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersMultiDeleteDialog open onOpenChange={onOpenChange} table={table} />
-    )
-
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(confirmDeleteInput, 'DELETE')
-    await expect.element(deleteButton).toBeEnabled()
-
-    await userEvent.click(deleteButton)
-
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-
-    await vi.waitFor(() => expect(resetRowSelection).toHaveBeenCalledOnce())
-  })
-
-  it('deletes successfully when press Enter key on the confirm delete input', async () => {
-    const { table, resetRowSelection } = createTableMock()
-    const onOpenChange = vi.fn()
-    const { getByRole } = await render(
-      <UsersMultiDeleteDialog open onOpenChange={onOpenChange} table={table} />
-    )
-
-    const confirmDeleteInput = getByRole('textbox', {
-      name: /Confirm by typing "DELETE"/i,
-    })
-    const deleteButton = getByRole('button', { name: /Delete/i })
-
-    await expect.element(deleteButton).toBeDisabled()
-
-    await userEvent.fill(confirmDeleteInput, 'DELETE')
-    await expect.element(deleteButton).toBeEnabled()
-
-    await userEvent.keyboard('{Enter}')
-    expect(onOpenChange).toHaveBeenCalledOnce()
-    expect(onOpenChange).toHaveBeenCalledWith(false)
-
-    await vi.waitFor(() => expect(resetRowSelection).toHaveBeenCalledOnce())
-  })
-})

+ 3 - 3
src/features/users/components/users-primary-buttons.tsx

@@ -1,4 +1,4 @@
-import { MailPlus, UserPlus } from 'lucide-react'
+import { UserPlus } from 'lucide-react'
 import { Button } from '@/components/ui/button'
 import { useUsers } from './users-provider'
 
@@ -6,13 +6,13 @@ export function UsersPrimaryButtons() {
   const { setOpen } = useUsers()
   return (
     <div className='flex gap-2'>
-      <Button
+      {/* <Button
         variant='outline'
         className='space-x-1'
         onClick={() => setOpen('invite')}
       >
         <span>دعوت کاربر</span> <MailPlus size={18} />
-      </Button>
+      </Button> */}
       <Button className='space-x-1' onClick={() => setOpen('add')}>
         <span>افزودن کاربر</span> <UserPlus size={18} />
       </Button>

+ 28 - 15
src/features/users/components/users-provider.tsx

@@ -1,36 +1,49 @@
-import React, { useState } from 'react'
-import useDialogState from '@/hooks/use-dialog-state'
-import { type User } from '../data/schema'
+import React, { createContext, useContext, useState } from 'react'
 
-type UsersDialogType = 'invite' | 'add' | 'edit' | 'delete'
+import { type User } from '@/types/users.types'
 
-type UsersContextType = {
+type UsersDialogType = 'add' | 'edit' | 'view' | 'delete' | 'multi-delete'
+
+interface UsersContextType {
   open: UsersDialogType | null
   setOpen: (str: UsersDialogType | null) => void
+
   currentRow: User | null
   setCurrentRow: React.Dispatch<React.SetStateAction<User | null>>
 }
 
-const UsersContext = React.createContext<UsersContextType | null>(null)
+const UsersContext = createContext<UsersContextType | undefined>(undefined)
+
+interface Props {
+  children: React.ReactNode
+}
+
+export function UsersProvider({ children }: Props) {
+  const [open, setOpen] = useState<UsersDialogType | null>(null)
 
-export function UsersProvider({ children }: { children: React.ReactNode }) {
-  const [open, setOpen] = useDialogState<UsersDialogType>(null)
   const [currentRow, setCurrentRow] = useState<User | null>(null)
 
   return (
-    <UsersContext value={{ open, setOpen, currentRow, setCurrentRow }}>
+    <UsersContext.Provider
+      value={{
+        open,
+        setOpen,
+        currentRow,
+        setCurrentRow,
+      }}
+    >
       {children}
-    </UsersContext>
+    </UsersContext.Provider>
   )
 }
 
 // eslint-disable-next-line react-refresh/only-export-components
-export const useUsers = () => {
-  const usersContext = React.useContext(UsersContext)
+export function useUsers() {
+  const context = useContext(UsersContext)
 
-  if (!usersContext) {
-    throw new Error('useUsers has to be used within <UsersContext>')
+  if (!context) {
+    throw new Error('useUsers must be used within UsersProvider')
   }
 
-  return usersContext
+  return context
 }

+ 4 - 24
src/features/users/components/users-table.tsx

@@ -6,9 +6,6 @@ import {
   getCoreRowModel,
   getFacetedRowModel,
   getFacetedUniqueValues,
-  getFilteredRowModel,
-  getPaginationRowModel,
-  getSortedRowModel,
   useReactTable,
 } from '@tanstack/react-table'
 import { cn } from '@/lib/utils'
@@ -22,7 +19,6 @@ import {
   TableRow,
 } from '@/components/ui/table'
 import { DataTablePagination, DataTableToolbar } from '@/components/data-table'
-import { roles } from '../data/data'
 import { DataTableBulkActions } from './data-table-bulk-actions'
 import { usersColumns as columns } from './users-columns'
 import { type User } from '@/types/users.types'
@@ -74,16 +70,16 @@ export function UsersTable({ data, search, navigate }: DataTableProps) {
       columnFilters,
       columnVisibility,
     },
+    manualPagination: true,
+    manualFiltering: true,
+    manualSorting: true,
     enableRowSelection: true,
     onPaginationChange,
     onColumnFiltersChange,
     onRowSelectionChange: setRowSelection,
     onSortingChange: setSorting,
     onColumnVisibilityChange: setColumnVisibility,
-    getPaginationRowModel: getPaginationRowModel(),
     getCoreRowModel: getCoreRowModel(),
-    getFilteredRowModel: getFilteredRowModel(),
-    getSortedRowModel: getSortedRowModel(),
     getFacetedRowModel: getFacetedRowModel(),
     getFacetedUniqueValues: getFacetedUniqueValues(),
   })
@@ -103,23 +99,7 @@ export function UsersTable({ data, search, navigate }: DataTableProps) {
         table={table}
         searchPlaceholder='فیلتر کاربران...'
         searchKey='username'
-        filters={[
-          {
-            columnId: 'status',
-            title: 'وضعیت',
-            options: [
-              { label: 'Active', value: 'active' },
-              { label: 'Inactive', value: 'inactive' },
-              { label: 'Invited', value: 'invited' },
-              { label: 'Suspended', value: 'suspended' },
-            ],
-          },
-          {
-            columnId: 'role',
-            title: 'نقش',
-            options: roles.map((role) => ({ ...role })),
-          },
-        ]}
+        filters={[]}
       />
       <div className='overflow-hidden rounded-md border'>
         <Table>

+ 0 - 35
src/features/users/data/data.ts

@@ -1,35 +0,0 @@
-import { Shield, UserCheck, Users, CreditCard } from 'lucide-react'
-import { type UserStatus } from './schema'
-
-export const callTypes = new Map<UserStatus, string>([
-  ['active', 'bg-teal-100/30 text-teal-900 dark:text-teal-200 border-teal-200'],
-  ['inactive', 'bg-neutral-300/40 border-neutral-300'],
-  ['invited', 'bg-sky-200/40 text-sky-900 dark:text-sky-100 border-sky-300'],
-  [
-    'suspended',
-    'bg-destructive/10 dark:bg-destructive/50 text-destructive dark:text-primary border-destructive/10',
-  ],
-])
-
-export const roles = [
-  {
-    label: 'Superadmin',
-    value: 'superadmin',
-    icon: Shield,
-  },
-  {
-    label: 'Admin',
-    value: 'admin',
-    icon: UserCheck,
-  },
-  {
-    label: 'Manager',
-    value: 'manager',
-    icon: Users,
-  },
-  {
-    label: 'Cashier',
-    value: 'cashier',
-    icon: CreditCard,
-  },
-] as const

+ 0 - 30
src/features/users/data/schema.ts

@@ -1,30 +0,0 @@
-import { z } from 'zod'
-
-const userStatusSchema = z.union([
-  z.literal('active'),
-  z.literal('inactive'),
-  z.literal('invited'),
-  z.literal('suspended'),
-])
-export type UserStatus = z.infer<typeof userStatusSchema>
-
-const userRoleSchema = z.union([
-  z.literal('superadmin'),
-  z.literal('admin'),
-  z.literal('cashier'),
-  z.literal('manager'),
-])
-
-const _userSchema = z.object({
-  id: z.string(),
-  first_name: z.string(),
-  last_name: z.string(),
-  username: z.string(),
-  email: z.string(),
-  phoneNumber: z.string(),
-  status: userStatusSchema,
-  role: userRoleSchema,
-  createdAt: z.coerce.date(),
-  updatedAt: z.coerce.date(),
-})
-export type User = z.infer<typeof _userSchema>

+ 0 - 33
src/features/users/data/users.ts

@@ -1,33 +0,0 @@
-import { faker } from '@faker-js/faker'
-
-// Set a fixed seed for consistent data generation
-faker.seed(67890)
-
-export const users = Array.from({ length: 500 }, () => {
-  const firstName = faker.person.firstName()
-  const lastName = faker.person.lastName()
-  return {
-    id: faker.string.uuid(),
-    firstName,
-    lastName,
-    username: faker.internet
-      .username({ firstName, lastName })
-      .toLocaleLowerCase(),
-    email: faker.internet.email({ firstName }).toLocaleLowerCase(),
-    phoneNumber: faker.phone.number({ style: 'international' }),
-    status: faker.helpers.arrayElement([
-      'active',
-      'inactive',
-      'invited',
-      'suspended',
-    ]),
-    role: faker.helpers.arrayElement([
-      'superadmin',
-      'admin',
-      'cashier',
-      'manager',
-    ]),
-    createdAt: faker.date.past(),
-    updatedAt: faker.date.recent(),
-  }
-})

+ 11 - 0
src/hooks/location/useCities.ts

@@ -0,0 +1,11 @@
+import { useQuery } from '@tanstack/react-query'
+import { locationService } from '@/services/location.service'
+import { queryKeys } from '@/core/react-query/keys'
+
+export function useCities(provinceId: number) {
+  return useQuery({
+    queryKey: queryKeys.location.cities(provinceId),
+    queryFn: () => locationService.cities(provinceId!),
+    enabled: !!provinceId,
+  })
+}

+ 10 - 0
src/hooks/location/useProvinces.ts

@@ -0,0 +1,10 @@
+import { useQuery } from '@tanstack/react-query'
+import { locationService } from '@/services/location.service'
+import { queryKeys } from '@/core/react-query/keys'
+
+export function useProvinces() {
+  return useQuery({
+    queryKey: queryKeys.location.provinces,
+    queryFn: locationService.provinces,
+  })
+}

+ 24 - 0
src/hooks/posts/useChangeStatePost.tsx

@@ -0,0 +1,24 @@
+import { queryKeys } from '@/core/react-query/keys'
+import { postsService } from '@/services/posts.service'
+import { type ChangeStatePost } from '@/types/posts.types'
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+type ChangePostStateVariables = {
+  id: number
+  payload: ChangeStatePost
+}
+
+export function useChangeStatePost() {
+  const queryClient = useQueryClient()
+
+  return useMutation({
+    mutationFn: ({ id, payload }: ChangePostStateVariables) =>
+      postsService.updateState(id, payload),
+
+    onSuccess: () => {
+      queryClient.invalidateQueries({
+        queryKey: queryKeys.posts.lists(),
+      })
+    },
+  })
+}

+ 12 - 0
src/hooks/posts/usePost.tsx

@@ -0,0 +1,12 @@
+import { useQuery } from '@tanstack/react-query'
+import { queryKeys } from '@/core/react-query/keys'
+import { postsService } from '@/services/posts.service'
+import { type Post } from '@/types/posts.types'
+
+export function usePost(postId: number | string) {
+  return useQuery<Post>({
+    queryKey: queryKeys.posts.detail(postId),
+    queryFn: () => postsService.detail(postId),
+    enabled: !!postId,
+  })
+}

+ 13 - 0
src/hooks/posts/usePosts.tsx

@@ -0,0 +1,13 @@
+import { queryKeys } from '@/core/react-query/keys'
+import { postsService } from '@/services/posts.service'
+import { type PaginationParams } from '@/types/common.types'
+import { type GetPostsResponse } from '@/types/posts.types'
+import { keepPreviousData, useQuery } from '@tanstack/react-query'
+
+export function usePosts(params: PaginationParams = { page: 1, per_page: 10 }) {
+  return useQuery<GetPostsResponse>({
+    queryKey: queryKeys.posts.list(params),
+    queryFn: () => postsService.list(params),
+    placeholderData: keepPreviousData,
+  })
+}

+ 78 - 0
src/hooks/posts/useVideoPlayer.tsx

@@ -0,0 +1,78 @@
+import { useEffect, useRef, useState } from 'react'
+
+export function useVideoPlayer() {
+  const videoRef = useRef<HTMLVideoElement>(null)
+
+  const [isPlaying, setIsPlaying] = useState(false)
+  const [isMuted, setIsMuted] = useState(true)
+  const [progress, setProgress] = useState(0)
+  const [isEnded, setIsEnded] = useState(false)
+
+  const togglePlay = () => {
+    const video = videoRef.current
+    if (!video) return
+
+    if (isEnded) {
+      video.currentTime = 0
+      setIsEnded(false)
+    }
+
+    if (video.paused) {
+      video.play()
+      setIsPlaying(true)
+    } else {
+      video.pause()
+      setIsPlaying(false)
+    }
+  }
+
+  const toggleMute = () => {
+    const video = videoRef.current
+    if (!video) return
+
+    video.muted = !video.muted
+    setIsMuted(video.muted)
+  }
+
+  const seek = (percent: number) => {
+    const video = videoRef.current
+    if (!video) return
+
+    video.currentTime = percent * video.duration
+  }
+
+  useEffect(() => {
+    const video = videoRef.current
+    if (!video) return
+
+    const updateProgress = () => {
+      const percent = (video.currentTime / video.duration) * 100
+      setProgress(percent)
+    }
+
+    const onEnded = () => {
+      setIsEnded(true)
+      setIsPlaying(false)
+    }
+
+    video.addEventListener('timeupdate', updateProgress)
+    video.addEventListener('ended', onEnded)
+
+    return () => {
+      video.removeEventListener('timeupdate', updateProgress)
+      video.removeEventListener('ended', onEnded)
+    }
+  }, [])
+
+  return {
+    videoRef,
+    isPlaying,
+    setIsPlaying,
+    isMuted,
+    progress,
+    isEnded,
+    togglePlay,
+    toggleMute,
+    seek,
+  }
+}

+ 17 - 0
src/hooks/users/useCreateUser.tsx

@@ -0,0 +1,17 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { usersService } from '@/services/users.service'
+import { queryKeys } from '@/core/react-query/keys'
+
+export function useCreateUser() {
+  const queryClient = useQueryClient()
+
+  return useMutation({
+    mutationFn: usersService.create,
+
+    onSuccess: () => {
+      queryClient.invalidateQueries({
+        queryKey: queryKeys.users.lists(),
+      })
+    },
+  })
+}

+ 18 - 0
src/hooks/users/useDeleteUser.tsx

@@ -0,0 +1,18 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import { usersService } from '@/services/users.service'
+import { queryKeys } from '@/core/react-query/keys'
+
+export function useDeleteUser() {
+  const queryClient = useQueryClient()
+
+  return useMutation({
+    mutationFn: (userId: number) => usersService.delete(userId),
+
+    onSuccess: () => {
+      queryClient.invalidateQueries({
+        queryKey: queryKeys.users.lists(),
+      })
+    },
+  })
+}

+ 24 - 0
src/hooks/users/useUpdateUser.tsx

@@ -0,0 +1,24 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { usersService } from '@/services/users.service'
+import { queryKeys } from '@/core/react-query/keys'
+import { type CreateUserPayload } from '@/types/users.types'
+
+type UpdateUserVariables = {
+  id: number
+  payload: CreateUserPayload
+}
+
+export function useUpdateUser() {
+  const queryClient = useQueryClient()
+
+  return useMutation({
+    mutationFn: ({ id, payload }: UpdateUserVariables) =>
+      usersService.update(id, payload),
+
+    onSuccess: () => {
+      queryClient.invalidateQueries({
+        queryKey: queryKeys.users.lists(),
+      })
+    },
+  })
+}

+ 2 - 2
src/hooks/users/useUser.tsx

@@ -1,11 +1,11 @@
 // src/hooks/users/useUser.ts
 import { useQuery } from '@tanstack/react-query'
 import { usersService } from '@/services/users.service'
-import type { GetUserDetailResponse } from '@/types/users.types'
+import type { User } from '@/types/users.types'
 import { queryKeys } from '@/core/react-query/keys'
 
 export function useUser(userId: number | string) {
-  return useQuery<GetUserDetailResponse>({
+  return useQuery<User>({
     queryKey: queryKeys.users.detail(userId),
     queryFn: () => usersService.detail(userId),
     enabled: !!userId,

+ 11 - 0
src/hooks/users/useUserDetail.tsx

@@ -0,0 +1,11 @@
+import { useQuery } from '@tanstack/react-query'
+import { usersService } from '@/services/users.service'
+import { queryKeys } from '@/core/react-query/keys'
+
+export function useUserDetail(userId: number) {
+  return useQuery({
+    queryKey: queryKeys.users.user(userId),
+    queryFn: () => usersService.detail(userId!),
+    enabled: !!userId,
+  })
+}

+ 15 - 0
src/lib/numberFromSelect.ts

@@ -0,0 +1,15 @@
+import { z } from 'zod'
+
+export const numberFromSelect = z.preprocess((value) => {
+  if (value === '' || value === null || value === undefined) {
+    return undefined
+  }
+  return Number(value)
+}, z.number().optional())
+
+export const optionalNumberFromSelect = z.preprocess((value) => {
+  if (value === '' || value === null || value === undefined) {
+    return undefined
+  }
+  return Number(value)
+}, z.number().optional())

+ 21 - 0
src/routeTree.gen.ts

@@ -24,6 +24,7 @@ import { Route as AuthenticatedSettingsRouteRouteImport } from './routes/_authen
 import { Route as AuthenticatedUsersIndexRouteImport } from './routes/_authenticated/users/index'
 import { Route as AuthenticatedTasksIndexRouteImport } from './routes/_authenticated/tasks/index'
 import { Route as AuthenticatedSettingsIndexRouteImport } from './routes/_authenticated/settings/index'
+import { Route as AuthenticatedPostsIndexRouteImport } from './routes/_authenticated/posts/index'
 import { Route as AuthenticatedHelpCenterIndexRouteImport } from './routes/_authenticated/help-center/index'
 import { Route as AuthenticatedChatsIndexRouteImport } from './routes/_authenticated/chats/index'
 import { Route as AuthenticatedAppsIndexRouteImport } from './routes/_authenticated/apps/index'
@@ -110,6 +111,11 @@ const AuthenticatedSettingsIndexRoute =
     path: '/',
     getParentRoute: () => AuthenticatedSettingsRouteRoute,
   } as any)
+const AuthenticatedPostsIndexRoute = AuthenticatedPostsIndexRouteImport.update({
+  id: '/posts/',
+  path: '/posts/',
+  getParentRoute: () => AuthenticatedRouteRoute,
+} as any)
 const AuthenticatedHelpCenterIndexRoute =
   AuthenticatedHelpCenterIndexRouteImport.update({
     id: '/help-center/',
@@ -194,6 +200,7 @@ export interface FileRoutesByFullPath {
   '/apps/': typeof AuthenticatedAppsIndexRoute
   '/chats/': typeof AuthenticatedChatsIndexRoute
   '/help-center/': typeof AuthenticatedHelpCenterIndexRoute
+  '/posts/': typeof AuthenticatedPostsIndexRoute
   '/settings/': typeof AuthenticatedSettingsIndexRoute
   '/tasks/': typeof AuthenticatedTasksIndexRoute
   '/users/': typeof AuthenticatedUsersIndexRoute
@@ -218,6 +225,7 @@ export interface FileRoutesByTo {
   '/apps': typeof AuthenticatedAppsIndexRoute
   '/chats': typeof AuthenticatedChatsIndexRoute
   '/help-center': typeof AuthenticatedHelpCenterIndexRoute
+  '/posts': typeof AuthenticatedPostsIndexRoute
   '/settings': typeof AuthenticatedSettingsIndexRoute
   '/tasks': typeof AuthenticatedTasksIndexRoute
   '/users': typeof AuthenticatedUsersIndexRoute
@@ -247,6 +255,7 @@ export interface FileRoutesById {
   '/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute
   '/_authenticated/chats/': typeof AuthenticatedChatsIndexRoute
   '/_authenticated/help-center/': typeof AuthenticatedHelpCenterIndexRoute
+  '/_authenticated/posts/': typeof AuthenticatedPostsIndexRoute
   '/_authenticated/settings/': typeof AuthenticatedSettingsIndexRoute
   '/_authenticated/tasks/': typeof AuthenticatedTasksIndexRoute
   '/_authenticated/users/': typeof AuthenticatedUsersIndexRoute
@@ -274,6 +283,7 @@ export interface FileRouteTypes {
     | '/apps/'
     | '/chats/'
     | '/help-center/'
+    | '/posts/'
     | '/settings/'
     | '/tasks/'
     | '/users/'
@@ -298,6 +308,7 @@ export interface FileRouteTypes {
     | '/apps'
     | '/chats'
     | '/help-center'
+    | '/posts'
     | '/settings'
     | '/tasks'
     | '/users'
@@ -326,6 +337,7 @@ export interface FileRouteTypes {
     | '/_authenticated/apps/'
     | '/_authenticated/chats/'
     | '/_authenticated/help-center/'
+    | '/_authenticated/posts/'
     | '/_authenticated/settings/'
     | '/_authenticated/tasks/'
     | '/_authenticated/users/'
@@ -449,6 +461,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSettingsIndexRouteImport
       parentRoute: typeof AuthenticatedSettingsRouteRoute
     }
+    '/_authenticated/posts/': {
+      id: '/_authenticated/posts/'
+      path: '/posts'
+      fullPath: '/posts/'
+      preLoaderRoute: typeof AuthenticatedPostsIndexRouteImport
+      parentRoute: typeof AuthenticatedRouteRoute
+    }
     '/_authenticated/help-center/': {
       id: '/_authenticated/help-center/'
       path: '/help-center'
@@ -559,6 +578,7 @@ interface AuthenticatedRouteRouteChildren {
   AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute
   AuthenticatedChatsIndexRoute: typeof AuthenticatedChatsIndexRoute
   AuthenticatedHelpCenterIndexRoute: typeof AuthenticatedHelpCenterIndexRoute
+  AuthenticatedPostsIndexRoute: typeof AuthenticatedPostsIndexRoute
   AuthenticatedTasksIndexRoute: typeof AuthenticatedTasksIndexRoute
   AuthenticatedUsersIndexRoute: typeof AuthenticatedUsersIndexRoute
 }
@@ -570,6 +590,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
   AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute,
   AuthenticatedChatsIndexRoute: AuthenticatedChatsIndexRoute,
   AuthenticatedHelpCenterIndexRoute: AuthenticatedHelpCenterIndexRoute,
+  AuthenticatedPostsIndexRoute: AuthenticatedPostsIndexRoute,
   AuthenticatedTasksIndexRoute: AuthenticatedTasksIndexRoute,
   AuthenticatedUsersIndexRoute: AuthenticatedUsersIndexRoute,
 }

+ 6 - 0
src/routes/_authenticated/posts/index.tsx

@@ -0,0 +1,6 @@
+import { Posts } from '@/features/posts'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_authenticated/posts/')({
+  component: Posts,
+})

+ 0 - 5
src/routes/_authenticated/users/index.tsx

@@ -1,7 +1,6 @@
 import z from 'zod'
 import { createFileRoute } from '@tanstack/react-router'
 import { Users } from '@/features/users'
-import { roles } from '@/features/users/data/data'
 
 const usersSearchSchema = z.object({
   page: z.number().optional().catch(1),
@@ -18,10 +17,6 @@ const usersSearchSchema = z.object({
     )
     .optional()
     .catch([]),
-  role: z
-    .array(z.enum(roles.map((r) => r.value as (typeof roles)[number]['value'])))
-    .optional()
-    .catch([]),
   // Per-column text filter (example for username)
   username: z.string().optional().catch(''),
 })

+ 20 - 0
src/services/location.service.ts

@@ -0,0 +1,20 @@
+import http from '@/core/api/http'
+import {
+  type GetCityResponse,
+  type GetProvinceResponse,
+} from '@/types/location.type'
+
+export const locationService = {
+  provinces: async () => {
+    const res = await http.get<GetProvinceResponse>('/public/provinces')
+    return res.data.provinces
+  },
+
+  cities: async (provinceId: number) => {
+    const res = await http.get<GetCityResponse>('/public/cities', {
+      params: { province_id: provinceId },
+    })
+
+    return res.data.cities
+  },
+}

+ 37 - 0
src/services/posts.service.ts

@@ -0,0 +1,37 @@
+import http from '@/core/api/http'
+
+import type { ApiResponse, PaginationParams } from '@/types/common.types'
+import type {
+  ChangeStatePost,
+  GetPostDetailResponse,
+  GetPostsResponse,
+} from '@/types/posts.types'
+
+const BASE_URL = '/admin/posts'
+
+export const postsService = {
+  list: async (params?: PaginationParams): Promise<GetPostsResponse> => {
+    const searchParams = new URLSearchParams()
+
+    if (params?.page) searchParams.set('page', String(params.page))
+    if (params?.per_page) searchParams.set('per_page', String(params.per_page))
+
+    const query = searchParams.toString()
+    const url = query ? `${BASE_URL}?${query}` : BASE_URL
+
+    return http.get<GetPostsResponse>(url)
+  },
+
+  detail: async (postId: number | string) => {
+    const res = await http.get<GetPostDetailResponse>(`${BASE_URL}/${postId}`)
+    return res?.data?.post
+  },
+
+  updateState: async (postId: number | string, payload: ChangeStatePost) => {
+    const res = await http.post<ApiResponse<void>>(
+      `${BASE_URL}/${postId}/state`,
+      payload
+    )
+    return res?.data
+  },
+}

+ 19 - 3
src/services/users.service.ts

@@ -2,8 +2,9 @@ import http from '@/core/api/http'
 import type {
   GetUserResponse,
   GetUserDetailResponse,
+  CreateUserPayload,
 } from '@/types/users.types'
-import type { PaginationParams } from '@/types/common.types'
+import type { ApiResponse, PaginationParams } from '@/types/common.types'
 
 const BASE_URL = '/admin/users'
 
@@ -20,7 +21,22 @@ export const usersService = {
     return http.get<GetUserResponse>(url)
   },
 
-  detail: async (userId: number | string): Promise<GetUserDetailResponse> => {
-    return http.get<GetUserDetailResponse>(`${BASE_URL}/${userId}`)
+  detail: async (userId: number | string) => {
+    const res = await http.get<GetUserDetailResponse>(`${BASE_URL}/${userId}`)
+    return res?.data?.user
+  },
+
+  create: async (payload: CreateUserPayload) => {
+    const res = await http.post<ApiResponse<void>>(BASE_URL, payload)
+    return res?.data
+  },
+
+  update: async (id: number, payload: CreateUserPayload) => {
+    const res = await http.put<ApiResponse<void>>(`${BASE_URL}/${id}`, payload)
+    return res?.data
+  },
+
+  delete: async (userId: number | string) => {
+    return http.delete(`${BASE_URL}/${userId}`)
   },
 }

+ 7 - 0
src/types/common.types.ts

@@ -2,3 +2,10 @@ export interface PaginationParams {
   page?: number
   per_page?: number
 }
+
+export type ApiResponse<T> = {
+  success: boolean
+  message: string
+  data: T | null
+  errors?: Record<string, string[]>
+}

+ 27 - 0
src/types/location.type.ts

@@ -0,0 +1,27 @@
+export type Province = {
+  id: number
+  name: string
+}
+
+export type City = {
+  id: number
+  name: string
+  province_id: number
+  province_name: string
+}
+
+export type GetProvinceResponse = {
+  success: boolean
+  message: string
+  data: {
+    provinces: Province[]
+  }
+}
+
+export type GetCityResponse = {
+  success: boolean
+  message: string
+  data: {
+    cities: City[]
+  }
+}

+ 71 - 0
src/types/posts.types.ts

@@ -0,0 +1,71 @@
+export interface Pagination {
+  current_page: number
+  last_page: number
+  per_page: number
+  total: number
+  from: number
+  to: number
+  has_more_pages: boolean
+  links: Links
+}
+
+export interface Links {
+  first: string
+  last: string
+  prev: null
+  next: string
+}
+
+export interface Post {
+  id: number
+  user_id: number
+  user: User
+  state: string
+  caption: string
+  video: Avatar
+  thumbnail: null
+  likes_count: number
+  dislikes_count: number
+  views_count: number
+  comment_count: number
+  created_at: string
+  updated_at: string
+}
+
+interface User {
+  id: number
+  first_name: string
+  last_name: string
+  full_name: string
+  username: string
+  avatar: Avatar | null
+}
+
+interface Avatar {
+  name: string
+  url: string
+  hash: string
+  type: string
+  entity_slug: string
+}
+
+export interface GetPostsResponse {
+  success: boolean
+  message: string
+  data: {
+    posts: Post[]
+    pagination: Pagination
+  }
+}
+
+export interface GetPostDetailResponse {
+  success: boolean
+  message: string
+  data: {
+    post: Post
+  }
+}
+
+export interface ChangeStatePost {
+  state: number
+}

+ 23 - 0
src/types/users.types.ts

@@ -77,3 +77,26 @@ export interface GetUserDetailResponse {
     posts: Post[]
   }
 }
+
+export interface CreateUserPayload {
+  phone: string
+  first_name: string
+  last_name: string
+  email?: string
+  username: string
+  province_id?: number
+  city_id?: number
+  gender: number
+  birth_date: string
+  weight?: number
+  height?: number
+  foot_specialization?: string
+  post_skill?: string
+  skill_level?: string
+  activity_history?: boolean
+  team_name?: string
+  favorite_iranian_team?: string
+  favorite_foreign_team?: string
+  shirt_number?: number
+  bio?: string
+}