4 コミット a20c579d64 ... 1251694556

作者 SHA1 メッセージ 日付
  Mohammad Mahdi Salimi 1251694556 feat: implement users api and hooks 1 ヶ月 前
  Mohammad Mahdi Salimi 20ed1bc42d feat: update auth store and route guard 1 ヶ月 前
  Mohammad Mahdi Salimi 827980e936 feat: update folder structure and route guard 1 ヶ月 前
  Mohammad Mahdi Salimi 43f31aa36f feat: implement user and guest guard 1 ヶ月 前
40 ファイル変更426 行追加636 行削除
  1. 0 28
      src/assets/brand-icons/icon-discord.tsx
  2. 0 33
      src/assets/brand-icons/icon-docker.tsx
  3. 0 25
      src/assets/brand-icons/icon-facebook.tsx
  4. 0 27
      src/assets/brand-icons/icon-figma.tsx
  5. 0 25
      src/assets/brand-icons/icon-github.tsx
  6. 0 25
      src/assets/brand-icons/icon-gitlab.tsx
  7. 0 28
      src/assets/brand-icons/icon-gmail.tsx
  8. 0 30
      src/assets/brand-icons/icon-medium.tsx
  9. 0 28
      src/assets/brand-icons/icon-notion.tsx
  10. 0 26
      src/assets/brand-icons/icon-skype.tsx
  11. 0 28
      src/assets/brand-icons/icon-slack.tsx
  12. 0 25
      src/assets/brand-icons/icon-stripe.tsx
  13. 0 25
      src/assets/brand-icons/icon-telegram.tsx
  14. 0 27
      src/assets/brand-icons/icon-trello.tsx
  15. 0 26
      src/assets/brand-icons/icon-whatsapp.tsx
  16. 0 26
      src/assets/brand-icons/icon-zoom.tsx
  17. 0 16
      src/assets/brand-icons/index.ts
  18. 0 41
      src/assets/clerk-full-logo.tsx
  19. 0 23
      src/assets/clerk-logo.tsx
  20. 30 17
      src/core/api/axios.ts
  21. 41 10
      src/core/api/http.ts
  22. 7 5
      src/core/react-query/keys.ts
  23. 13 10
      src/features/auth/sign-in/components/user-auth-form.tsx
  24. 0 4
      src/features/users/api/getUsers.ts
  25. 1 1
      src/features/users/components/users-table.tsx
  26. 0 15
      src/features/users/hooks/useUsers.tsx
  27. 3 2
      src/features/users/index.tsx
  28. 0 1
      src/features/users/types.ts
  29. 17 0
      src/hooks/auth/useAuth.tsx
  30. 13 0
      src/hooks/users/useUser.tsx
  31. 13 0
      src/hooks/users/useUsers.tsx
  32. 1 1
      src/main.tsx
  33. 40 2
      src/routes/(auth)/sign-in.tsx
  34. 57 1
      src/routes/_authenticated/route.tsx
  35. 23 0
      src/services/auth.service.ts
  36. 26 0
      src/services/users.service.ts
  37. 45 55
      src/stores/auth-store.ts
  38. 13 0
      src/types/auth.types.ts
  39. 4 0
      src/types/common.types.ts
  40. 79 0
      src/types/users.types.ts

+ 0 - 28
src/assets/brand-icons/icon-discord.tsx

@@ -1,28 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconDiscord({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Discord</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M8 12a1 1 0 1 0 2 0a1 1 0 0 0 -2 0' />
-      <path d='M14 12a1 1 0 1 0 2 0a1 1 0 0 0 -2 0' />
-      <path d='M15.5 17c0 1 1.5 3 2 3c1.5 0 2.833 -1.667 3.5 -3c.667 -1.667 .5 -5.833 -1.5 -11.5c-1.457 -1.015 -3 -1.34 -4.5 -1.5l-.972 1.923a11.913 11.913 0 0 0 -4.053 0l-.975 -1.923c-1.5 .16 -3.043 .485 -4.5 1.5c-2 5.667 -2.167 9.833 -1.5 11.5c.667 1.333 2 3 3.5 3c.5 0 2 -2 2 -3' />
-      <path d='M7 16.5c3.5 1 6.5 1 10 0' />
-    </svg>
-  )
-}

+ 0 - 33
src/assets/brand-icons/icon-docker.tsx

@@ -1,33 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconDocker({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Docker</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z' />
-      <path d='M5 10h3v3h-3z' />
-      <path d='M8 10h3v3h-3z' />
-      <path d='M11 10h3v3h-3z' />
-      <path d='M8 7h3v3h-3z' />
-      <path d='M11 7h3v3h-3z' />
-      <path d='M11 4h3v3h-3z' />
-      <path d='M4.571 18c1.5 0 2.047 -.074 2.958 -.78' />
-      <path d='M10 16l0 .01' />
-    </svg>
-  )
-}

+ 0 - 25
src/assets/brand-icons/icon-facebook.tsx

@@ -1,25 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconFacebook({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Facebook</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M7 10v4h3v7h4v-7h3l1 -4h-4v-2a1 1 0 0 1 1 -1h3v-4h-3a5 5 0 0 0 -5 5v2h-3' />
-    </svg>
-  )
-}

+ 0 - 27
src/assets/brand-icons/icon-figma.tsx

@@ -1,27 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconFigma({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Figma</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M15 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0' />
-      <path d='M6 3m0 3a3 3 0 0 1 3 -3h6a3 3 0 0 1 3 3v0a3 3 0 0 1 -3 3h-6a3 3 0 0 1 -3 -3z' />
-      <path d='M9 9a3 3 0 0 0 0 6h3m-3 0a3 3 0 1 0 3 3v-15' />
-    </svg>
-  )
-}

+ 0 - 25
src/assets/brand-icons/icon-github.tsx

@@ -1,25 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconGithub({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>GitHub</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5' />
-    </svg>
-  )
-}

+ 0 - 25
src/assets/brand-icons/icon-gitlab.tsx

@@ -1,25 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconGitlab({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>GitLab</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M21 14l-9 7l-9 -7l3 -11l3 7h6l3 -7z' />
-    </svg>
-  )
-}

+ 0 - 28
src/assets/brand-icons/icon-gmail.tsx

@@ -1,28 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconGmail({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Gmail</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M16 20h3a1 1 0 0 0 1 -1v-14a1 1 0 0 0 -1 -1h-3v16z' />
-      <path d='M5 20h3v-16h-3a1 1 0 0 0 -1 1v14a1 1 0 0 0 1 1z' />
-      <path d='M16 4l-4 4l-4 -4' />
-      <path d='M4 6.5l8 7.5l8 -7.5' />
-    </svg>
-  )
-}

+ 0 - 30
src/assets/brand-icons/icon-medium.tsx

@@ -1,30 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconMedium({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Medium</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z' />
-      <path d='M8 9h1l3 3l3 -3h1' />
-      <path d='M8 15l2 0' />
-      <path d='M14 15l2 0' />
-      <path d='M9 9l0 6' />
-      <path d='M15 9l0 6' />
-    </svg>
-  )
-}

+ 0 - 28
src/assets/brand-icons/icon-notion.tsx

@@ -1,28 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconNotion({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Notion</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M11 17.5v-6.5h.5l4 6h.5v-6.5' />
-      <path d='M19.077 20.071l-11.53 .887a1 1 0 0 1 -.876 -.397l-2.471 -3.294a1 1 0 0 1 -.2 -.6v-10.741a1 1 0 0 1 .923 -.997l11.389 -.876a2 2 0 0 1 1.262 .33l1.535 1.023a2 2 0 0 1 .891 1.664v12.004a1 1 0 0 1 -.923 .997z' />
-      <path d='M4.5 5.5l2.5 2.5' />
-      <path d='M20 7l-13 1v12.5' />
-    </svg>
-  )
-}

+ 0 - 26
src/assets/brand-icons/icon-skype.tsx

@@ -1,26 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconSkype({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Skype</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M12 3a9 9 0 0 1 8.603 11.65a4.5 4.5 0 0 1 -5.953 5.953a9 9 0 0 1 -11.253 -11.253a4.5 4.5 0 0 1 5.953 -5.954a8.987 8.987 0 0 1 2.65 -.396z' />
-      <path d='M8 14.5c.5 2 2.358 2.5 4 2.5c2.905 0 4 -1.187 4 -2.5c0 -1.503 -1.927 -2.5 -4 -2.5s-4 -1 -4 -2.5c0 -1.313 1.095 -2.5 4 -2.5c1.642 0 3.5 .5 4 2.5' />
-    </svg>
-  )
-}

+ 0 - 28
src/assets/brand-icons/icon-slack.tsx

@@ -1,28 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconSlack({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Slack</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M12 12v-6a2 2 0 0 1 4 0v6m0 -2a2 2 0 1 1 2 2h-6' />
-      <path d='M12 12h6a2 2 0 0 1 0 4h-6m2 0a2 2 0 1 1 -2 2v-6' />
-      <path d='M12 12v6a2 2 0 0 1 -4 0v-6m0 2a2 2 0 1 1 -2 -2h6' />
-      <path d='M12 12h-6a2 2 0 0 1 0 -4h6m-2 0a2 2 0 1 1 2 -2v6' />
-    </svg>
-  )
-}

+ 0 - 25
src/assets/brand-icons/icon-stripe.tsx

@@ -1,25 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconStripe({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Stripe</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M11.453 8.056c0 -.623 .518 -.979 1.442 -.979c1.69 0 3.41 .343 4.605 .923l.5 -4c-.948 -.449 -2.82 -1 -5.5 -1c-1.895 0 -3.373 .087 -4.5 1c-1.172 .956 -2 2.33 -2 4c0 3.03 1.958 4.906 5 6c1.961 .69 3 .743 3 1.5c0 .735 -.851 1.5 -2 1.5c-1.423 0 -3.963 -.609 -5.5 -1.5l-.5 4c1.321 .734 3.474 1.5 6 1.5c2 0 3.957 -.468 5.084 -1.36c1.263 -.979 1.916 -2.268 1.916 -4.14c0 -3.096 -1.915 -4.547 -5 -5.637c-1.646 -.605 -2.544 -1.07 -2.544 -1.807z' />
-    </svg>
-  )
-}

+ 0 - 25
src/assets/brand-icons/icon-telegram.tsx

@@ -1,25 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconTelegram({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Telegram</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4' />
-    </svg>
-  )
-}

+ 0 - 27
src/assets/brand-icons/icon-trello.tsx

@@ -1,27 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconTrello({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Trello</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z' />
-      <path d='M7 7h3v10h-3z' />
-      <path d='M14 7h3v6h-3z' />
-    </svg>
-  )
-}

+ 0 - 26
src/assets/brand-icons/icon-whatsapp.tsx

@@ -1,26 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconWhatsapp({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>WhatsApp</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9' />
-      <path d='M9 10a.5 .5 0 0 0 1 0v-1a.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a.5 .5 0 0 0 0 -1h-1a.5 .5 0 0 0 0 1' />
-    </svg>
-  )
-}

+ 0 - 26
src/assets/brand-icons/icon-zoom.tsx

@@ -1,26 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function IconZoom({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      width='24'
-      height='24'
-      className={cn('[&>path]:stroke-current', className)}
-      fill='none'
-      stroke='currentColor'
-      strokeWidth='2'
-      strokeLinecap='round'
-      strokeLinejoin='round'
-      {...props}
-    >
-      <title>Zoom</title>
-      <path strokeWidth='0' d='M0 0h24v24H0z' fill='none' />
-      <path d='M17.011 9.385v5.128l3.989 3.487v-12z' />
-      <path d='M3.887 6h10.08c1.468 0 3.033 1.203 3.033 2.803v8.196a.991 .991 0 0 1 -.975 1h-10.373c-1.667 0 -2.652 -1.5 -2.652 -3l.01 -8a.882 .882 0 0 1 .208 -.71a.841 .841 0 0 1 .67 -.287z' />
-    </svg>
-  )
-}

+ 0 - 16
src/assets/brand-icons/index.ts

@@ -1,16 +0,0 @@
-export { IconDiscord } from './icon-discord'
-export { IconDocker } from './icon-docker'
-export { IconFacebook } from './icon-facebook'
-export { IconFigma } from './icon-figma'
-export { IconGithub } from './icon-github'
-export { IconGitlab } from './icon-gitlab'
-export { IconGmail } from './icon-gmail'
-export { IconMedium } from './icon-medium'
-export { IconNotion } from './icon-notion'
-export { IconSkype } from './icon-skype'
-export { IconSlack } from './icon-slack'
-export { IconStripe } from './icon-stripe'
-export { IconTelegram } from './icon-telegram'
-export { IconTrello } from './icon-trello'
-export { IconWhatsapp } from './icon-whatsapp'
-export { IconZoom } from './icon-zoom'

+ 0 - 41
src/assets/clerk-full-logo.tsx

@@ -1,41 +0,0 @@
-import { type SVGProps } from 'react'
-
-export function ClerkFullLogo(props: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      width={77}
-      height={24}
-      viewBox='0 0 77 24'
-      fill='none'
-      xmlns='http://www.w3.org/2000/svg'
-      {...props}
-    >
-      <path
-        d='M35.148 16.738a4.198 4.198 0 01-3.06 1.283 3.53 3.53 0 01-2.604-1.034c-.619-.645-.975-1.566-.975-2.665 0-2.199 1.432-3.703 3.58-3.703a3.914 3.914 0 013.034 1.377l1.859-1.644c-1.211-1.47-3.176-2.229-5.042-2.229-3.652 0-6.24 2.517-6.24 6.22 0 1.831.643 3.374 1.728 4.463s2.631 1.728 4.415 1.728c2.317 0 4.166-.94 5.203-2.122l-1.898-1.674zM38.727 3.428h2.766V20.34h-2.766V3.428zM54.818 15.283c.046-.368.07-.74.076-1.11 0-3.507-2.296-6.047-5.847-6.047a5.738 5.738 0 00-4.215 1.725c-1.038 1.089-1.66 2.631-1.66 4.47 0 3.749 2.642 6.216 6.146 6.216 2.35 0 4.043-.951 5.058-2.242l-1.812-1.605-.09-.076a3.749 3.749 0 01-3.008 1.406c-1.778 0-3.061-1.037-3.427-2.737h8.779zm-8.733-2.22a3.365 3.365 0 01.737-1.449 3.082 3.082 0 012.368-.996c1.58 0 2.57.988 2.911 2.445h-6.016zM63.445 8.09v3.084a13.36 13.36 0 00-.838-.05c-2.094 0-3.282 1.505-3.282 3.479v5.736h-2.763V8.261h2.763v1.83h.025c.938-1.283 2.284-1.997 3.75-1.997l.345-.004zM69.887 15.281l-1.998 2.222v2.837h-2.764V3.428h2.764v10.374L72.822 8.3h3.283l-4.341 4.86 4.417 7.18h-3.11l-3.133-5.059h-.051z'
-        fill='#1F0256'
-      />
-      <path
-        d='M19.116 3.16l-2.88 2.881a.571.571 0 01-.701.084 6.854 6.854 0 00-10.39 5.647 6.867 6.867 0 00.979 3.764.571.571 0 01-.084.699l-2.88 2.88a.57.57 0 01-.865-.063A11.994 11.994 0 0119.051 2.295a.57.57 0 01.065.866z'
-        fill='url(#paint0_linear_26568_214324)'
-      />
-      <path
-        d='M19.113 20.829l-2.88-2.88a.571.571 0 00-.7-.085 6.854 6.854 0 01-7.081 0 .571.571 0 00-.7.084l-2.881 2.88a.57.57 0 00.062.877 11.994 11.994 0 0014.114 0 .571.571 0 00.066-.876zM11.997 15.422a3.427 3.427 0 100-6.854 3.427 3.427 0 000 6.854z'
-        fill='#1F0256'
-      />
-      <defs>
-        <linearGradient
-          id='paint0_linear_26568_214324'
-          x1={16.4087}
-          y1={-1.75881}
-          x2={-7.88473}
-          y2={22.5365}
-          gradientUnits='userSpaceOnUse'
-        >
-          <stop stopColor='#17CCFC' />
-          <stop offset={0.5} stopColor='#5D31FF' />
-          <stop offset={1} stopColor='#F35AFF' />
-        </linearGradient>
-      </defs>
-    </svg>
-  )
-}

+ 0 - 23
src/assets/clerk-logo.tsx

@@ -1,23 +0,0 @@
-import { type SVGProps } from 'react'
-import { cn } from '@/lib/utils'
-
-export function ClerkLogo({ className, ...props }: SVGProps<SVGSVGElement>) {
-  return (
-    <svg
-      role='img'
-      viewBox='0 0 24 24'
-      xmlns='http://www.w3.org/2000/svg'
-      id='clerk'
-      height='24'
-      width='24'
-      className={cn('[&>path]:fill-foreground', className)}
-      {...props}
-    >
-      <title>Clerk</title>
-      <path
-        d='m21.47 20.829 -2.881 -2.881a0.572 0.572 0 0 0 -0.7 -0.084 6.854 6.854 0 0 1 -7.081 0 0.576 0.576 0 0 0 -0.7 0.084l-2.881 2.881a0.576 0.576 0 0 0 -0.103 0.69 0.57 0.57 0 0 0 0.166 0.186 12 12 0 0 0 14.113 0 0.58 0.58 0 0 0 0.239 -0.423 0.576 0.576 0 0 0 -0.172 -0.453Zm0.002 -17.668 -2.88 2.88a0.569 0.569 0 0 1 -0.701 0.084A6.857 6.857 0 0 0 8.724 8.08a6.862 6.862 0 0 0 -1.222 3.692 6.86 6.86 0 0 0 0.978 3.764 0.573 0.573 0 0 1 -0.083 0.699l-2.881 2.88a0.567 0.567 0 0 1 -0.864 -0.063A11.993 11.993 0 0 1 6.771 2.7a11.99 11.99 0 0 1 14.637 -0.405 0.566 0.566 0 0 1 0.232 0.418 0.57 0.57 0 0 1 -0.168 0.448Zm-7.118 12.261a3.427 3.427 0 1 0 0 -6.854 3.427 3.427 0 0 0 0 6.854Z'
-        strokeWidth='1'
-      ></path>
-    </svg>
-  )
-}

+ 30 - 17
src/core/api/axios.ts

@@ -1,36 +1,49 @@
-import { useAuthStore } from '@/stores/auth-store'
 import axios from 'axios'
+import { useAuthStore } from '@/stores/auth-store'
 
-const api = axios.create({
-  baseURL:
-    import.meta.env.VITE_API_BASE_URL || 'https://api.talentyar.net/api/v1',
-  // withCredentials: true,
+export const api = axios.create({
+  baseURL: import.meta.env.VITE_API_BASE_URL,
+  timeout: 5 * 60 * 1000,
   headers: {
     'Content-Type': 'application/json',
     Accept: 'application/json',
   },
 })
 
-api.interceptors.request.use((config) => {
-  const token = useAuthStore.getState().token
+// Request Interceptor
+api.interceptors.request.use(
+  (config) => {
+    const { token } = useAuthStore.getState()
 
-  if (token) {
-    config.headers.Authorization = `Bearer ${token}`
-  }
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`
+    }
 
-  return config
-})
+    return config
+  },
+  (error) => {
+    return Promise.reject(error)
+  }
+)
 
+// Response Interceptor
 api.interceptors.response.use(
   (response) => response,
-  (error) => {
-    if (error.response?.status === 401) {
-      const logout = useAuthStore.getState().logout
+  async (error) => {
+    const status = error?.response?.status
+
+    if (status === 401) {
+      const { logout } = useAuthStore.getState()
+
       logout()
+
+      if (window.location.pathname !== '/sign-in') {
+        const currentPath = window.location.pathname + window.location.search
+
+        window.location.href = `/sign-in?redirect=${encodeURIComponent(currentPath)}`
+      }
     }
 
     return Promise.reject(error)
   }
 )
-
-export default api

+ 41 - 10
src/core/api/http.ts

@@ -1,16 +1,47 @@
 import type { AxiosRequestConfig } from 'axios'
-import api from './axios'
+import { api } from './axios'
 
-export const http = {
-  get: <T>(url: string, config?: AxiosRequestConfig) =>
-    api.get<T>(url, config).then((res) => res.data),
+async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
+  const response = await api.get<T>(url, config)
+  return response.data
+}
+
+async function post<T, B = unknown>(
+  url: string,
+  body?: B,
+  config?: AxiosRequestConfig
+): Promise<T> {
+  const response = await api.post<T>(url, body, config)
+  return response.data
+}
 
-  post: <T, B = unknown>(url: string, body?: B, config?: AxiosRequestConfig) =>
-    api.post<T>(url, body, config).then((res) => res.data),
+async function put<T, B = unknown>(
+  url: string,
+  body?: B,
+  config?: AxiosRequestConfig
+): Promise<T> {
+  const response = await api.put<T>(url, body, config)
+  return response.data
+}
 
-  put: <T, B = unknown>(url: string, body?: B, config?: AxiosRequestConfig) =>
-    api.put<T>(url, body, config).then((res) => res.data),
+async function patch<T, B = unknown>(
+  url: string,
+  body?: B,
+  config?: AxiosRequestConfig
+): Promise<T> {
+  const response = await api.patch<T>(url, body, config)
+  return response.data
+}
+
+async function del<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
+  const response = await api.delete<T>(url, config)
+  return response.data
+}
 
-  delete: <T>(url: string, config?: AxiosRequestConfig) =>
-    api.delete<T>(url, config).then((res) => res.data),
+export default {
+  get,
+  post,
+  put,
+  patch,
+  delete: del,
 }

+ 7 - 5
src/core/react-query/keys.ts

@@ -1,10 +1,12 @@
 export const queryKeys = {
-  roles: {
-    all: ['roles'] as const,
-  },
-
   users: {
     all: ['users'] as const,
-    // search: (username: string) => ['users', 'search', username] as const,
+
+    lists: () => [...queryKeys.users.all, 'list'] as const,
+    list: (params: unknown) => [...queryKeys.users.lists(), params] as const,
+
+    details: () => [...queryKeys.users.all, 'detail'] as const,
+    detail: (id: number | string) =>
+      [...queryKeys.users.details(), id] as const,
   },
 }

+ 13 - 10
src/features/auth/sign-in/components/user-auth-form.tsx

@@ -1,4 +1,3 @@
-import { useState } from 'react'
 import { z } from 'zod'
 import { useForm } from 'react-hook-form'
 import { zodResolver } from '@hookform/resolvers/zod'
@@ -6,7 +5,7 @@ import { useNavigate } from '@tanstack/react-router'
 import { Loader2, LogIn } from 'lucide-react'
 import { toast } from 'sonner'
 
-import { useAuthStore } from '@/stores/auth-store'
+import { useLogin } from '@/hooks/auth/useAuth'
 import { cn } from '@/lib/utils'
 import { Button } from '@/components/ui/button'
 import {
@@ -40,9 +39,8 @@ export function UserAuthForm({
   redirectTo,
   ...props
 }: UserAuthFormProps) {
-  const [isLoading, setIsLoading] = useState(false)
   const navigate = useNavigate()
-  const login = useAuthStore((s) => s.login)
+  const { mutateAsync: login, isPending } = useLogin()
 
   const form = useForm<z.infer<typeof formSchema>>({
     resolver: zodResolver(formSchema),
@@ -53,16 +51,17 @@ export function UserAuthForm({
   })
 
   const onSubmit = async (data: z.infer<typeof formSchema>) => {
-    setIsLoading(true)
     try {
       await login(data)
 
       toast.success('ورود با موفقیت انجام شد')
-      navigate({ to: redirectTo || '/', replace: true })
+
+      navigate({
+        to: redirectTo || '/',
+        replace: true,
+      })
     } catch {
       toast.error('نام کاربری یا رمز عبور اشتباه است')
-    } finally {
-      setIsLoading(false)
     }
   }
 
@@ -79,6 +78,7 @@ export function UserAuthForm({
           render={({ field }) => (
             <FormItem>
               <FormLabel>نام کاربری</FormLabel>
+
               <FormControl>
                 <Input
                   placeholder='نام کاربری یا شماره موبایل'
@@ -86,6 +86,7 @@ export function UserAuthForm({
                   {...field}
                 />
               </FormControl>
+
               <FormMessage />
             </FormItem>
           )}
@@ -97,6 +98,7 @@ export function UserAuthForm({
           render={({ field }) => (
             <FormItem>
               <FormLabel>رمز عبور</FormLabel>
+
               <FormControl>
                 <PasswordInput
                   placeholder='********'
@@ -104,13 +106,14 @@ export function UserAuthForm({
                   {...field}
                 />
               </FormControl>
+
               <FormMessage />
             </FormItem>
           )}
         />
 
-        <Button className='mt-2' disabled={isLoading}>
-          {isLoading ? (
+        <Button className='mt-2' disabled={isPending}>
+          {isPending ? (
             <Loader2 className='mr-2 h-4 w-4 animate-spin' />
           ) : (
             <LogIn className='mr-2 h-4 w-4' />

+ 0 - 4
src/features/users/api/getUsers.ts

@@ -1,4 +0,0 @@
-import { http } from '@/core/api/http'
-import { type GetUserResponse } from '../types'
-
-export const getUsers = () => http.get<GetUserResponse>('/admin/users')

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

@@ -23,9 +23,9 @@ import {
 } from '@/components/ui/table'
 import { DataTablePagination, DataTableToolbar } from '@/components/data-table'
 import { roles } from '../data/data'
-import { type User } from '../data/schema'
 import { DataTableBulkActions } from './data-table-bulk-actions'
 import { usersColumns as columns } from './users-columns'
+import { type User } from '@/types/users.types'
 
 type DataTableProps = {
   data: User[]

+ 0 - 15
src/features/users/hooks/useUsers.tsx

@@ -1,15 +0,0 @@
-import { queryKeys } from '@/core/react-query/keys'
-import { useQuery } from '@tanstack/react-query'
-import { getUsers } from '../api/getUsers'
-
-export const useUsers = (enabled?: boolean) => {
-  return useQuery({
-    queryKey: queryKeys.users.all,
-    queryFn: async () => {
-      const res = await getUsers()
-      return res.data?.users
-    },
-    staleTime: 1000 * 60 * 2,
-    enabled,
-  })
-}

+ 3 - 2
src/features/users/index.tsx

@@ -9,7 +9,7 @@ import { UsersDialogs } from './components/users-dialogs'
 import { UsersPrimaryButtons } from './components/users-primary-buttons'
 import { UsersProvider } from './components/users-provider'
 import { UsersTable } from './components/users-table'
-import { useUsers } from './hooks/useUsers'
+import { useUsers } from '../../hooks/users/useUsers'
 
 const route = getRouteApi('/_authenticated/users/')
 
@@ -17,7 +17,8 @@ export function Users() {
   const search = route.useSearch()
   const navigate = route.useNavigate()
 
-  const { data: users } = useUsers()
+  const { data } = useUsers({ page: 1, per_page: 10 })
+  const users = data?.data.users
 
   return (
     <UsersProvider>

+ 0 - 1
src/features/users/types.ts

@@ -1 +0,0 @@
-export type GetUserResponse = any

+ 17 - 0
src/hooks/auth/useAuth.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query'
+import { authService } from '@/services/auth.service'
+import { useAuthStore } from '@/stores/auth-store'
+
+export function useLogin() {
+  const setToken = useAuthStore((s) => s.setToken)
+  const setUser = useAuthStore((s) => s.setUser)
+
+  return useMutation({
+    mutationFn: authService.login,
+
+    onSuccess: (data) => {
+      setToken(data.token)
+      setUser(data.admin)
+    },
+  })
+}

+ 13 - 0
src/hooks/users/useUser.tsx

@@ -0,0 +1,13 @@
+// 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 { queryKeys } from '@/core/react-query/keys'
+
+export function useUser(userId: number | string) {
+  return useQuery<GetUserDetailResponse>({
+    queryKey: queryKeys.users.detail(userId),
+    queryFn: () => usersService.detail(userId),
+    enabled: !!userId,
+  })
+}

+ 13 - 0
src/hooks/users/useUsers.tsx

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

+ 1 - 1
src/main.tsx

@@ -53,7 +53,7 @@ const queryClient = new QueryClient({
       if (error instanceof AxiosError) {
         if (error.response?.status === 401) {
           toast.error('Session expired!')
-          useAuthStore.getState().auth.reset()
+          useAuthStore.getState().logout()
           const redirect = `${router.history.location.href}`
           router.navigate({ to: '/sign-in', search: { redirect } })
         }

+ 40 - 2
src/routes/(auth)/sign-in.tsx

@@ -1,12 +1,50 @@
 import { z } from 'zod'
-import { createFileRoute } from '@tanstack/react-router'
+import { createFileRoute, redirect } from '@tanstack/react-router'
 import { SignIn } from '@/features/auth/sign-in'
+import { useAuthStore } from '@/stores/auth-store'
 
 const searchSchema = z.object({
   redirect: z.string().optional(),
 })
 
 export const Route = createFileRoute('/(auth)/sign-in')({
-  component: SignIn,
   validateSearch: searchSchema,
+
+  beforeLoad: async () => {
+    const auth = useAuthStore.getState()
+
+    if (!auth.isHydrated) {
+      await new Promise<void>((resolve) => {
+        const unsubscribe = useAuthStore.subscribe((state) => {
+          if (state.isHydrated) {
+            unsubscribe()
+            resolve()
+          }
+        })
+      })
+    }
+
+    const { token, isAuthenticated } = useAuthStore.getState()
+
+    const persisted = localStorage.getItem('auth-storage')
+
+    let hasPersistedToken = false
+
+    if (persisted) {
+      try {
+        const parsed = JSON.parse(persisted)
+        hasPersistedToken = !!parsed?.state?.token
+      } catch {
+        hasPersistedToken = false
+      }
+    }
+
+    if (token && isAuthenticated && hasPersistedToken) {
+      throw redirect({
+        to: '/',
+      })
+    }
+  },
+
+  component: SignIn,
 })

+ 57 - 1
src/routes/_authenticated/route.tsx

@@ -1,6 +1,62 @@
-import { createFileRoute } from '@tanstack/react-router'
+import { createFileRoute, redirect } from '@tanstack/react-router'
 import { AuthenticatedLayout } from '@/components/layout/authenticated-layout'
+import { useAuthStore } from '@/stores/auth-store'
+import { authService } from '@/services/auth.service'
 
 export const Route = createFileRoute('/_authenticated')({
+  beforeLoad: async ({ location }) => {
+    const auth = useAuthStore.getState()
+
+    if (!auth.isHydrated) {
+      await new Promise<void>((resolve) => {
+        const unsubscribe = useAuthStore.subscribe((state) => {
+          if (state.isHydrated) {
+            unsubscribe()
+            resolve()
+          }
+        })
+      })
+    }
+
+    const authState = useAuthStore.getState()
+
+    const {
+      token,
+      isAuthenticated,
+      profileFetched,
+      logout,
+      setUser,
+      setProfileFetched,
+    } = authState
+
+    if (!token || !isAuthenticated) {
+      logout()
+
+      throw redirect({
+        to: '/sign-in',
+        search: {
+          redirect: location.href,
+        },
+      })
+    }
+
+    if (!profileFetched) {
+      try {
+        const profile = await authService.profile()
+
+        setUser(profile.admin)
+        setProfileFetched(true)
+      } catch {
+        logout()
+        throw redirect({
+          to: '/sign-in',
+          search: {
+            redirect: location.href,
+          },
+        })
+      }
+    }
+  },
+
   component: AuthenticatedLayout,
 })

+ 23 - 0
src/services/auth.service.ts

@@ -0,0 +1,23 @@
+import http from '@/core/api/http'
+import {
+  type ProfileResponse,
+  type LoginPayload,
+  type LoginResponse,
+} from '@/types/auth.types'
+
+export const authService = {
+  login: async (data: LoginPayload) => {
+    const res = await http.post<LoginResponse, LoginPayload>(
+      'admin/auth/login',
+      data
+    )
+    return res.data
+  },
+
+  profile: async () => {
+    const res = await http.get<ProfileResponse>('admin/auth/profile')
+    return res.data
+  },
+
+  logout: () => http.post('admin/auth/logout'),
+}

+ 26 - 0
src/services/users.service.ts

@@ -0,0 +1,26 @@
+import http from '@/core/api/http'
+import type {
+  GetUserResponse,
+  GetUserDetailResponse,
+} from '@/types/users.types'
+import type { PaginationParams } from '@/types/common.types'
+
+const BASE_URL = '/admin/users'
+
+export const usersService = {
+  list: async (params?: PaginationParams): Promise<GetUserResponse> => {
+    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<GetUserResponse>(url)
+  },
+
+  detail: async (userId: number | string): Promise<GetUserDetailResponse> => {
+    return http.get<GetUserDetailResponse>(`${BASE_URL}/${userId}`)
+  },
+}

+ 45 - 55
src/stores/auth-store.ts

@@ -1,85 +1,71 @@
+import { authService } from '@/services/auth.service'
+import { type AdminUser } from '@/types/auth.types'
 import { create } from 'zustand'
 import { persist } from 'zustand/middleware'
-import type { AdminUser, LoginResponse } from '@/types/auth'
-import { http } from '@/core/api/http'
-
-interface LoginPayload {
-  username: string
-  password: string
-}
 
 interface AuthState {
   user: AdminUser | null
   token: string | null
   isAuthenticated: boolean
-  loading: boolean
-  error: string | null
-  login: (data: LoginPayload) => Promise<void>
-  logout: () => void
-  fetchProfile: () => Promise<void>
+  isHydrated: boolean
+  profileFetched: boolean
+  setToken: (token: string | null) => void
+  setUser: (user: AdminUser | null) => void
+  setHydrated: (state: boolean) => void
+  setProfileFetched: (state: boolean) => void
+  clearAuth: () => void
+  logout: () => Promise<void>
 }
 
 export const useAuthStore = create<AuthState>()(
   persist(
     (set) => ({
-      user: null,
       token: null,
-      isAuthenticated: false,
-      loading: false,
-      error: null,
-
-      login: async (data: LoginPayload) => {
-        set({ loading: true, error: null })
+      user: null,
 
-        try {
-          const response = await http.post<LoginResponse>(
-            '/admin/auth/login',
-            data
-          )
+      isAuthenticated: false,
+      isHydrated: false,
+      profileFetched: false,
 
-          const { token, admin } = response.data
+      setToken: (token) =>
+        set({
+          token,
+          isAuthenticated: !!token,
+        }),
 
-          set({
-            token,
-            user: admin,
-            isAuthenticated: true,
-            loading: false,
-            error: null,
-          })
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        } catch (error: any) {
-          const message = error?.response?.data?.message || 'Login failed'
+      setUser: (user) =>
+        set({
+          user,
+        }),
 
-          set({
-            loading: false,
-            error: message,
-          })
+      setHydrated: (state) =>
+        set({
+          isHydrated: state,
+        }),
 
-          // eslint-disable-next-line preserve-caught-error
-          throw new Error(message)
-        }
-      },
+      setProfileFetched: (state) =>
+        set({
+          profileFetched: state,
+        }),
 
-      logout: () => {
+      clearAuth: () =>
         set({
           user: null,
           token: null,
           isAuthenticated: false,
-          error: null,
-        })
-      },
+          profileFetched: false,
+        }),
 
-      fetchProfile: async () => {
+      logout: async () => {
         try {
-          const res = await http.get<{ data: AdminUser }>('/admin/auth/profile')
-
+          await authService.logout()
+        } finally {
           set({
-            user: res.data,
-            isAuthenticated: true,
+            user: null,
+            token: null,
+            isAuthenticated: false,
+            profileFetched: false,
           })
-        } catch (error) {
-          // eslint-disable-next-line no-console
-          console.log('Profile fetch failed', error)
         }
       },
     }),
@@ -91,6 +77,10 @@ export const useAuthStore = create<AuthState>()(
         token: state.token,
         isAuthenticated: state.isAuthenticated,
       }),
+
+      onRehydrateStorage: () => (state) => {
+        state?.setHydrated(true)
+      },
     }
   )
 )

+ 13 - 0
src/types/auth.ts → src/types/auth.types.ts

@@ -7,6 +7,11 @@ export interface AdminUser {
   roles: string[]
 }
 
+export interface LoginPayload {
+  username: string
+  password: string
+}
+
 export interface LoginResponse {
   success: boolean
   message: string
@@ -15,3 +20,11 @@ export interface LoginResponse {
     admin: AdminUser
   }
 }
+
+export interface ProfileResponse {
+  success: boolean
+  message: string
+  data: {
+    admin: AdminUser
+  }
+}

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

@@ -0,0 +1,4 @@
+export interface PaginationParams {
+  page?: number
+  per_page?: number
+}

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

@@ -0,0 +1,79 @@
+export interface User {
+  id: number
+  is_me: boolean
+  first_name: string
+  last_name: string
+  full_name: string
+  phone: string
+  email: null
+  username: string
+  province_id: number
+  city_id: number
+  gender: number
+  birth_date: string
+  weight: number
+  height: number
+  foot_specialization: string
+  post_skill: string
+  skill_level: string
+  activity_history: number
+  team_name: null
+  favorite_iranian_team: string
+  favorite_foreign_team: string
+  shirt_number: number
+  bio: null
+  followers_count: number
+  following_count: number
+  post_count: number
+  avatar?: Avatar
+  created_at: string
+  updated_at: string
+}
+
+export interface UsersListMeta {
+  total?: number
+  per_page?: number
+  current_page?: number
+  last_page?: number
+}
+
+export interface GetUserResponse {
+  success: boolean
+  message: string
+  data: {
+    users: User[]
+    meta?: UsersListMeta
+  }
+}
+
+export interface Avatar {
+  name: string
+  url: string
+  hash: string
+  type: string
+  entity_slug: string
+}
+
+export interface Post {
+  id: number
+  user_id: number
+  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
+}
+
+export interface GetUserDetailResponse {
+  success: boolean
+  message: string
+  data: {
+    user: User
+    posts: Post[]
+  }
+}