Browse Source

feat: implement referral codes page

Mohammad Mahdi Salimi 9 hours ago
parent
commit
eef7725b2d

+ 1 - 1
index.html

@@ -29,7 +29,7 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 
     <!-- Primary Meta Tags -->
-    <title>تلنت‌یار | Talentyar</title>
+    <title>ادمین تلنت‌یار | Talentyar Admin</title>
     <meta name="title" content="Talentyar Admin" />
     <meta
       name="description"

+ 6 - 1
src/components/layout/data/sidebar-data.ts

@@ -1,4 +1,4 @@
-import { LayoutDashboard, Users, Play, Star } from 'lucide-react'
+import { LayoutDashboard, Users, Play, Star, Tags } from 'lucide-react'
 import { type SidebarData } from '../types'
 import Logo from '@/assets/logo.svg'
 
@@ -34,6 +34,11 @@ export const sidebarData: SidebarData = {
           url: '/evaluations',
           icon: Star,
         },
+        {
+          title: 'کد دعوت',
+          url: '/referral-codes',
+          icon: Tags,
+        },
         // {
         //   title: 'کامنت ها',
         //   url: '/chats',

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

@@ -43,4 +43,12 @@ export const queryKeys = {
     items: () => [...queryKeys.evaluations.all, 'item'] as const,
     item: () => [...queryKeys.evaluations.items()] as const,
   },
+
+  referralCodes: {
+    all: ['referralCodes'] as const,
+
+    lists: () => [...queryKeys.referralCodes.all, 'list'] as const,
+    list: (params: unknown) =>
+      [...queryKeys.referralCodes.lists(), params] as const,
+  },
 }

+ 143 - 0
src/features/referral-codes/components/data-table-bulk-actions.tsx

@@ -0,0 +1,143 @@
+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 { type Referralcode } from '@/types/referral-codes.types'
+import { ReferralCodesMultiDeleteDialog } from './referral-codes-multi-delete-dialog'
+
+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 Referralcode
+    )
+    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 Referralcode
+    )
+    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>
+
+      <ReferralCodesMultiDeleteDialog
+        table={table}
+        open={showDeleteConfirm}
+        onOpenChange={setShowDeleteConfirm}
+      />
+    </>
+  )
+}

+ 75 - 0
src/features/referral-codes/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 { useReferralCodes } from './referral-codes-provider'
+import { type Referralcode } from '@/types/referral-codes.types'
+
+interface Props {
+  row: Row<Referralcode>
+}
+
+export function DataTableRowActions({ row }: Props) {
+  const { setOpen, setCurrentRow } = useReferralCodes()
+
+  const referralCode = row.original
+
+  const handleChangeState = () => {
+    setCurrentRow(referralCode)
+    setOpen('update')
+  }
+
+  const handleView = () => {
+    setCurrentRow(referralCode)
+    setOpen('view')
+  }
+
+  const handleDelete = () => {
+    setCurrentRow(referralCode)
+    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>
+  )
+}

+ 123 - 0
src/features/referral-codes/components/referral-codes-columns.tsx

@@ -0,0 +1,123 @@
+import { type ColumnDef } from '@tanstack/react-table'
+
+import { cn } from '@/lib/utils'
+
+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 Referralcode } from '@/types/referral-codes.types'
+
+export const referralCodesColumns: ColumnDef<Referralcode>[] = [
+  {
+    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: 'referral_code',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='کد تخفیف' />
+    ),
+    cell: ({ row }) => (
+      <div className='max-w-36 ps-3'>{row.original.referral_code}</div>
+    ),
+  },
+
+  {
+    accessorKey: 'username',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='نام کاربری' />
+    ),
+    cell: ({ row }) => (
+      <LongText className='max-w-36 ps-3'>
+        {row.original.owner.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: 'full_name',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='نام کامل' />
+    ),
+    cell: ({ row }) => (
+      <div className='max-w-36 ps-3'>{row.original.owner.full_name}</div>
+    ),
+  },
+
+  {
+    accessorKey: 'used_count',
+    header: ({ column }) => (
+      <DataTableColumnHeader column={column} title='تعداد استفاده' />
+    ),
+    cell: ({ row }) => (
+      <div className='max-w-36 ps-3 text-center'>{row.original.used_count}</div>
+    ),
+  },
+
+  {
+    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,
+  // },
+]

+ 57 - 0
src/features/referral-codes/components/referral-codes-delete-dialog.tsx

@@ -0,0 +1,57 @@
+import { AlertTriangle } from 'lucide-react'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { ConfirmDialog } from '@/components/confirm-dialog'
+import { type Referralcode } from '@/types/referral-codes.types'
+
+interface Props {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  currentRow: Referralcode | null
+}
+
+export function ReferralCodesDeleteDialog({
+  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='referral-codes-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
+    />
+  )
+}

+ 25 - 0
src/features/referral-codes/components/referral-codes-dialogs.tsx

@@ -0,0 +1,25 @@
+import { ReferralCodesDeleteDialog } from './referral-codes-delete-dialog'
+import { useReferralCodes } from './referral-codes-provider'
+
+export function ReferralCodesDialogs() {
+  const { open, setOpen, currentRow, setCurrentRow } = useReferralCodes()
+
+  const handleClose = () => {
+    setOpen(null)
+    setCurrentRow(null)
+  }
+
+  return (
+    <>
+      {/* <ReferralCodesActionDialog /> */}
+
+      <ReferralCodesDeleteDialog
+        open={open === 'delete'}
+        onOpenChange={(state) => {
+          if (!state) handleClose()
+        }}
+        currentRow={currentRow}
+      />
+    </>
+  )
+}

+ 103 - 0
src/features/referral-codes/components/referral-codes-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 ReferralCodesMultiDeleteDialogProps<TData> = {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  table: Table<TData>
+}
+
+const CONFIRM_WORD = 'DELETE'
+
+export function ReferralCodesMultiDeleteDialog<TData>({
+  open,
+  onOpenChange,
+  table,
+}: ReferralCodesMultiDeleteDialogProps<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 referral codes...',
+      success: () => {
+        setValue('')
+        table.resetRowSelection()
+        return `Deleted ${selectedRows.length} ${
+          selectedRows.length > 1 ? 'referral codes' : 'referral code'
+        }`
+      },
+      error: 'Error',
+    })
+  }
+
+  return (
+    <ConfirmDialog
+      open={open}
+      onOpenChange={onOpenChange}
+      form='referral-codes-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 ? 'referral codes' : 'referral code'}
+        </span>
+      }
+      desc={
+        <form
+          id='referral-codes-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 referral codes? <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
+    />
+  )
+}

+ 51 - 0
src/features/referral-codes/components/referral-codes-provider.tsx

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

+ 182 - 0
src/features/referral-codes/components/referral-codes-table.tsx

@@ -0,0 +1,182 @@
+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 } from '@/components/data-table'
+import { DataTableBulkActions } from './data-table-bulk-actions'
+import { referralCodesColumns as columns } from './referral-codes-columns'
+import type { Pagination } from '@/types/common.types'
+import { type Referralcode } from '@/types/referral-codes.types'
+
+type DataTableProps = {
+  data: Referralcode[]
+  paginationData?: Pagination
+  search: Record<string, unknown>
+  navigate: NavigateFn
+}
+
+export function ReferralCodesTable({
+  data,
+  paginationData,
+  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,
+    },
+    pageCount: paginationData?.last_page ?? -1,
+    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>
+  )
+}

+ 60 - 0
src/features/referral-codes/index.tsx

@@ -0,0 +1,60 @@
+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 { Main } from '@/components/layout/main'
+import { ReferralCodesProvider } from './components/referral-codes-provider'
+import { ReferralCodesTable } from './components/referral-codes-table'
+import { useReferralCodes } from '@/hooks/referral-codes/useReferralCodes'
+
+const route = getRouteApi('/_authenticated/referral-codes/')
+
+export function ReferralCodes() {
+  const search = route.useSearch()
+  const navigate = route.useNavigate()
+
+  const { data, isLoading } = useReferralCodes({
+    page: search.page ?? 1,
+    per_page: search.pageSize ?? 10,
+  })
+  const referralCodes = data?.data?.referral_codes
+  const paginationInfo = data?.data?.pagination
+
+  return (
+    <ReferralCodesProvider>
+      <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>
+        ) : (
+          <ReferralCodesTable
+            data={referralCodes ?? []}
+            paginationData={paginationInfo}
+            search={search}
+            navigate={navigate}
+          />
+        )}
+      </Main>
+
+      {/* <ReferralCodesDialogs /> */}
+    </ReferralCodesProvider>
+  )
+}

+ 15 - 0
src/hooks/referral-codes/useReferralCodes.tsx

@@ -0,0 +1,15 @@
+import { queryKeys } from '@/core/react-query/keys'
+import { referralCodesService } from '@/services/referral-codes.service'
+import { type PaginationParams } from '@/types/common.types'
+import { type GetReferralCodesResponse } from '@/types/referral-codes.types'
+import { keepPreviousData, useQuery } from '@tanstack/react-query'
+
+export function useReferralCodes(
+  params: PaginationParams = { page: 1, per_page: 10 }
+) {
+  return useQuery<GetReferralCodesResponse>({
+    queryKey: queryKeys.referralCodes.list(params),
+    queryFn: () => referralCodesService.list(params),
+    placeholderData: keepPreviousData,
+  })
+}

+ 22 - 0
src/routeTree.gen.ts

@@ -21,6 +21,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 AuthenticatedReferralCodesIndexRouteImport } from './routes/_authenticated/referral-codes/index'
 import { Route as AuthenticatedPostsIndexRouteImport } from './routes/_authenticated/posts/index'
 import { Route as AuthenticatedHelpCenterIndexRouteImport } from './routes/_authenticated/help-center/index'
 import { Route as AuthenticatedEvaluationsIndexRouteImport } from './routes/_authenticated/evaluations/index'
@@ -92,6 +93,12 @@ const AuthenticatedSettingsIndexRoute =
     path: '/',
     getParentRoute: () => AuthenticatedSettingsRouteRoute,
   } as any)
+const AuthenticatedReferralCodesIndexRoute =
+  AuthenticatedReferralCodesIndexRouteImport.update({
+    id: '/referral-codes/',
+    path: '/referral-codes/',
+    getParentRoute: () => AuthenticatedRouteRoute,
+  } as any)
 const AuthenticatedPostsIndexRoute = AuthenticatedPostsIndexRouteImport.update({
   id: '/posts/',
   path: '/posts/',
@@ -163,6 +170,7 @@ export interface FileRoutesByFullPath {
   '/evaluations/': typeof AuthenticatedEvaluationsIndexRoute
   '/help-center/': typeof AuthenticatedHelpCenterIndexRoute
   '/posts/': typeof AuthenticatedPostsIndexRoute
+  '/referral-codes/': typeof AuthenticatedReferralCodesIndexRoute
   '/settings/': typeof AuthenticatedSettingsIndexRoute
   '/tasks/': typeof AuthenticatedTasksIndexRoute
   '/users/': typeof AuthenticatedUsersIndexRoute
@@ -184,6 +192,7 @@ export interface FileRoutesByTo {
   '/evaluations': typeof AuthenticatedEvaluationsIndexRoute
   '/help-center': typeof AuthenticatedHelpCenterIndexRoute
   '/posts': typeof AuthenticatedPostsIndexRoute
+  '/referral-codes': typeof AuthenticatedReferralCodesIndexRoute
   '/settings': typeof AuthenticatedSettingsIndexRoute
   '/tasks': typeof AuthenticatedTasksIndexRoute
   '/users': typeof AuthenticatedUsersIndexRoute
@@ -208,6 +217,7 @@ export interface FileRoutesById {
   '/_authenticated/evaluations/': typeof AuthenticatedEvaluationsIndexRoute
   '/_authenticated/help-center/': typeof AuthenticatedHelpCenterIndexRoute
   '/_authenticated/posts/': typeof AuthenticatedPostsIndexRoute
+  '/_authenticated/referral-codes/': typeof AuthenticatedReferralCodesIndexRoute
   '/_authenticated/settings/': typeof AuthenticatedSettingsIndexRoute
   '/_authenticated/tasks/': typeof AuthenticatedTasksIndexRoute
   '/_authenticated/users/': typeof AuthenticatedUsersIndexRoute
@@ -232,6 +242,7 @@ export interface FileRouteTypes {
     | '/evaluations/'
     | '/help-center/'
     | '/posts/'
+    | '/referral-codes/'
     | '/settings/'
     | '/tasks/'
     | '/users/'
@@ -253,6 +264,7 @@ export interface FileRouteTypes {
     | '/evaluations'
     | '/help-center'
     | '/posts'
+    | '/referral-codes'
     | '/settings'
     | '/tasks'
     | '/users'
@@ -276,6 +288,7 @@ export interface FileRouteTypes {
     | '/_authenticated/evaluations/'
     | '/_authenticated/help-center/'
     | '/_authenticated/posts/'
+    | '/_authenticated/referral-codes/'
     | '/_authenticated/settings/'
     | '/_authenticated/tasks/'
     | '/_authenticated/users/'
@@ -377,6 +390,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSettingsIndexRouteImport
       parentRoute: typeof AuthenticatedSettingsRouteRoute
     }
+    '/_authenticated/referral-codes/': {
+      id: '/_authenticated/referral-codes/'
+      path: '/referral-codes'
+      fullPath: '/referral-codes/'
+      preLoaderRoute: typeof AuthenticatedReferralCodesIndexRouteImport
+      parentRoute: typeof AuthenticatedRouteRoute
+    }
     '/_authenticated/posts/': {
       id: '/_authenticated/posts/'
       path: '/posts'
@@ -474,6 +494,7 @@ interface AuthenticatedRouteRouteChildren {
   AuthenticatedEvaluationsIndexRoute: typeof AuthenticatedEvaluationsIndexRoute
   AuthenticatedHelpCenterIndexRoute: typeof AuthenticatedHelpCenterIndexRoute
   AuthenticatedPostsIndexRoute: typeof AuthenticatedPostsIndexRoute
+  AuthenticatedReferralCodesIndexRoute: typeof AuthenticatedReferralCodesIndexRoute
   AuthenticatedTasksIndexRoute: typeof AuthenticatedTasksIndexRoute
   AuthenticatedUsersIndexRoute: typeof AuthenticatedUsersIndexRoute
 }
@@ -486,6 +507,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
   AuthenticatedEvaluationsIndexRoute: AuthenticatedEvaluationsIndexRoute,
   AuthenticatedHelpCenterIndexRoute: AuthenticatedHelpCenterIndexRoute,
   AuthenticatedPostsIndexRoute: AuthenticatedPostsIndexRoute,
+  AuthenticatedReferralCodesIndexRoute: AuthenticatedReferralCodesIndexRoute,
   AuthenticatedTasksIndexRoute: AuthenticatedTasksIndexRoute,
   AuthenticatedUsersIndexRoute: AuthenticatedUsersIndexRoute,
 }

+ 13 - 0
src/routes/_authenticated/referral-codes/index.tsx

@@ -0,0 +1,13 @@
+import z from 'zod'
+import { createFileRoute } from '@tanstack/react-router'
+import { ReferralCodes } from '@/features/referral-codes'
+
+const referralCodesSearchSchema = z.object({
+  page: z.coerce.number().optional().catch(1),
+  pageSize: z.coerce.number().optional().catch(10),
+})
+
+export const Route = createFileRoute('/_authenticated/referral-codes/')({
+  validateSearch: referralCodesSearchSchema,
+  component: ReferralCodes,
+})

+ 22 - 0
src/services/referral-codes.service.ts

@@ -0,0 +1,22 @@
+import http from '@/core/api/http'
+
+import type { PaginationParams } from '@/types/common.types'
+import type { GetReferralCodesResponse } from '@/types/referral-codes.types'
+
+const BASE_URL = '/admin/users/referral-codes'
+
+export const referralCodesService = {
+  list: async (
+    params?: PaginationParams
+  ): Promise<GetReferralCodesResponse> => {
+    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<GetReferralCodesResponse>(url)
+  },
+}

+ 28 - 0
src/types/referral-codes.types.ts

@@ -0,0 +1,28 @@
+import { type Pagination } from './common.types'
+
+export interface Referralcode {
+  referral_code: string
+  used_count: number
+  owner: Owner
+  created_at: string
+  updated_at: string
+}
+
+interface Owner {
+  id: number
+  first_name: null | string
+  last_name: null | string
+  full_name: null | string
+  username: null | string
+  phone: string
+  email: null
+}
+
+export interface GetReferralCodesResponse {
+  success: boolean
+  message: string
+  data: {
+    referral_codes: Referralcode[]
+    pagination: Pagination
+  }
+}