Jelajahi Sumber

feat: implement user and guest guard

Mohammad Mahdi Salimi 1 bulan lalu
induk
melakukan
43f31aa36f

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

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

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

@@ -1,6 +1,56 @@
-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'
 
 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 persisted = localStorage.getItem('auth-storage')
+
+    const hasPersistedToken = persisted && JSON.parse(persisted)?.state?.token
+
+    const { token, isAuthenticated, fetchProfile, logout, profileFetched } =
+      authState
+
+    if (!token || !isAuthenticated || !hasPersistedToken) {
+      logout()
+
+      throw redirect({
+        to: '/sign-in',
+        search: {
+          redirect: location.href,
+        },
+      })
+    }
+
+    if (!profileFetched) {
+      try {
+        await fetchProfile()
+      } catch {
+        logout()
+        throw redirect({
+          to: '/sign-in',
+          search: {
+            redirect: location.href,
+          },
+        })
+      }
+    }
+  },
+
   component: AuthenticatedLayout,
 })

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

@@ -0,0 +1,17 @@
+import http from '@/core/api/http'
+import { type LoginPayload, type LoginResponse } from '@/types/auth.types'
+
+export interface AuthUser {
+  id: string
+  name: string
+  email: string
+}
+
+export const authService = {
+  login: (data: LoginPayload) =>
+    http.post<LoginResponse, LoginPayload>('/auth/login', data),
+
+  profile: () => http.get<AuthUser>('/auth/profile'),
+
+  logout: () => http.post('/auth/logout'),
+}

+ 29 - 2
src/stores/auth-store.ts

@@ -1,6 +1,6 @@
 import { create } from 'zustand'
 import { persist } from 'zustand/middleware'
-import type { AdminUser, LoginResponse } from '@/types/auth'
+import type { AdminUser, LoginResponse } from '@/types/auth.types'
 import { http } from '@/core/api/http'
 
 interface LoginPayload {
@@ -14,6 +14,9 @@ interface AuthState {
   isAuthenticated: boolean
   loading: boolean
   error: string | null
+  isHydrated: boolean
+  profileFetched: boolean
+
   login: (data: LoginPayload) => Promise<void>
   logout: () => void
   fetchProfile: () => Promise<void>
@@ -21,12 +24,14 @@ interface AuthState {
 
 export const useAuthStore = create<AuthState>()(
   persist(
-    (set) => ({
+    (set, get) => ({
       user: null,
       token: null,
       isAuthenticated: false,
       loading: false,
       error: null,
+      isHydrated: false,
+      profileFetched: false,
 
       login: async (data: LoginPayload) => {
         set({ loading: true, error: null })
@@ -45,6 +50,7 @@ export const useAuthStore = create<AuthState>()(
             isAuthenticated: true,
             loading: false,
             error: null,
+            profileFetched: true,
           })
           // eslint-disable-next-line @typescript-eslint/no-explicit-any
         } catch (error: any) {
@@ -66,20 +72,35 @@ export const useAuthStore = create<AuthState>()(
           token: null,
           isAuthenticated: false,
           error: null,
+          profileFetched: false,
         })
       },
 
       fetchProfile: async () => {
+        const { profileFetched } = get()
+
+        if (profileFetched) return
+
         try {
           const res = await http.get<{ data: AdminUser }>('/admin/auth/profile')
 
           set({
             user: res.data,
             isAuthenticated: true,
+            profileFetched: true,
           })
         } catch (error) {
           // eslint-disable-next-line no-console
           console.log('Profile fetch failed', error)
+
+          set({
+            user: null,
+            token: null,
+            isAuthenticated: false,
+            profileFetched: false,
+          })
+
+          throw error
         }
       },
     }),
@@ -91,6 +112,12 @@ export const useAuthStore = create<AuthState>()(
         token: state.token,
         isAuthenticated: state.isAuthenticated,
       }),
+
+      onRehydrateStorage: () => (state) => {
+        if (state) {
+          state.isHydrated = true
+        }
+      },
     }
   )
 )

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