Jelajahi Sumber

feat: implmenet posts crud

Mohammad Mahdi Salimi 1 bulan lalu
induk
melakukan
0f7bac9cbb
34 mengubah file dengan 1993 tambahan dan 869 penghapusan
  1. 80 99
      src/components/layout/data/sidebar-data.ts
  2. 7 16
      src/components/profile-dropdown.tsx
  3. 11 0
      src/core/react-query/keys.ts
  4. 2 195
      src/features/dashboard/index.tsx
  5. 139 0
      src/features/posts/components/data-table-bulk-actions.tsx
  6. 75 0
      src/features/posts/components/data-table-row-actions.tsx
  7. 122 0
      src/features/posts/components/posts-action-dialog.tsx
  8. 118 0
      src/features/posts/components/posts-columns.tsx
  9. 53 0
      src/features/posts/components/posts-delete-dialog.tsx
  10. 27 0
      src/features/posts/components/posts-dialogs.tsx
  11. 103 0
      src/features/posts/components/posts-multi-delete-dialog.tsx
  12. 47 0
      src/features/posts/components/posts-provider.tsx
  13. 174 0
      src/features/posts/components/posts-table.tsx
  14. 110 0
      src/features/posts/components/video-player.tsx
  15. 50 0
      src/features/posts/index.tsx
  16. 1 1
      src/features/users/components/data-table-bulk-actions.tsx
  17. 574 284
      src/features/users/components/users-action-dialog.tsx
  18. 0 150
      src/features/users/components/users-invite-dialog.tsx
  19. 1 18
      src/features/users/components/users-table.tsx
  20. 0 35
      src/features/users/data/data.ts
  21. 0 30
      src/features/users/data/schema.ts
  22. 0 33
      src/features/users/data/users.ts
  23. 24 0
      src/hooks/posts/useChangeStatePost.tsx
  24. 12 0
      src/hooks/posts/usePost.tsx
  25. 13 0
      src/hooks/posts/usePosts.tsx
  26. 78 0
      src/hooks/posts/useVideoPlayer.tsx
  27. 8 1
      src/hooks/users/useUpdateUser.tsx
  28. 2 2
      src/hooks/users/useUser.tsx
  29. 21 0
      src/routeTree.gen.ts
  30. 6 0
      src/routes/_authenticated/posts/index.tsx
  31. 0 5
      src/routes/_authenticated/users/index.tsx
  32. 37 0
      src/services/posts.service.ts
  33. 27 0
      src/types/location.type.ts
  34. 71 0
      src/types/posts.types.ts

+ 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)}>

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

@@ -11,6 +11,17 @@ export const queryKeys = {
       [...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,

+ 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>

+ 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>
   )

+ 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>
-  )
-}

+ 1 - 18
src/features/users/components/users-table.tsx

@@ -19,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'
@@ -100,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(),
-  }
-})

+ 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,
+  }
+}

+ 8 - 1
src/hooks/users/useUpdateUser.tsx

@@ -1,12 +1,19 @@
 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 }: any) => usersService.update(id, payload),
+    mutationFn: ({ id, payload }: UpdateUserVariables) =>
+      usersService.update(id, payload),
 
     onSuccess: () => {
       queryClient.invalidateQueries({

+ 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,

+ 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(''),
 })

+ 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
+  },
+}

+ 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
+}