user-management.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { useEffect, useState } from 'react'
  2. import {
  3. createFileRoute,
  4. Link,
  5. useNavigate,
  6. useRouter,
  7. } from '@tanstack/react-router'
  8. import { SignedIn, useAuth, UserButton } from '@clerk/clerk-react'
  9. import { ExternalLink, Loader2 } from 'lucide-react'
  10. import { ClerkLogo } from '@/assets/clerk-logo'
  11. import { Button } from '@/components/ui/button'
  12. import { Header } from '@/components/layout/header'
  13. import { Main } from '@/components/layout/main'
  14. import { LearnMore } from '@/components/learn-more'
  15. import { Search } from '@/components/search'
  16. import { ThemeSwitch } from '@/components/theme-switch'
  17. import { columns } from '@/features/users/components/users-columns'
  18. import { UsersDialogs } from '@/features/users/components/users-dialogs'
  19. import { UsersPrimaryButtons } from '@/features/users/components/users-primary-buttons'
  20. import { UsersTable } from '@/features/users/components/users-table'
  21. import UsersProvider from '@/features/users/context/users-context'
  22. import { userListSchema } from '@/features/users/data/schema'
  23. import { users } from '@/features/users/data/users'
  24. export const Route = createFileRoute('/clerk/_authenticated/user-management')({
  25. component: UserManagement,
  26. })
  27. function UserManagement() {
  28. const [opened, setOpened] = useState(true)
  29. const { isLoaded, isSignedIn } = useAuth()
  30. if (!isLoaded) {
  31. return (
  32. <div className='flex h-svh items-center justify-center'>
  33. <Loader2 className='size-8 animate-spin' />
  34. </div>
  35. )
  36. }
  37. if (!isSignedIn) {
  38. return <Unauthorized />
  39. }
  40. // Parse user list
  41. const userList = userListSchema.parse(users)
  42. return (
  43. <>
  44. <SignedIn>
  45. <UsersProvider>
  46. <Header fixed>
  47. <Search />
  48. <div className='ms-auto flex items-center space-x-4'>
  49. <ThemeSwitch />
  50. <UserButton />
  51. </div>
  52. </Header>
  53. <Main>
  54. <div className='mb-2 flex flex-wrap items-center justify-between space-y-2'>
  55. <div>
  56. <h2 className='text-2xl font-bold tracking-tight'>User List</h2>
  57. <div className='flex gap-1'>
  58. <p className='text-muted-foreground'>
  59. Manage your users and their roles here.
  60. </p>
  61. <LearnMore
  62. open={opened}
  63. onOpenChange={setOpened}
  64. contentProps={{ side: 'right' }}
  65. >
  66. <p>
  67. This is the same as{' '}
  68. <Link
  69. to='/users'
  70. className='text-blue-500 underline decoration-dashed underline-offset-2'
  71. >
  72. '/users'
  73. </Link>
  74. </p>
  75. <p className='mt-4'>
  76. You can sign out or manage/delete your account via the
  77. User Profile menu in the top-right corner of the page.
  78. <ExternalLink className='inline-block size-4' />
  79. </p>
  80. </LearnMore>
  81. </div>
  82. </div>
  83. <UsersPrimaryButtons />
  84. </div>
  85. <div className='-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-y-0 lg:space-x-12'>
  86. <UsersTable data={userList} columns={columns} />
  87. </div>
  88. </Main>
  89. <UsersDialogs />
  90. </UsersProvider>
  91. </SignedIn>
  92. </>
  93. )
  94. }
  95. const COUNTDOWN = 5 // Countdown second
  96. function Unauthorized() {
  97. const navigate = useNavigate()
  98. const { history } = useRouter()
  99. const [opened, setOpened] = useState(true)
  100. const [cancelled, setCancelled] = useState(false)
  101. const [countdown, setCountdown] = useState(COUNTDOWN)
  102. // Set and run the countdown conditionally
  103. useEffect(() => {
  104. if (cancelled || opened) return
  105. const interval = setInterval(() => {
  106. setCountdown((prev) => (prev > 0 ? prev - 1 : 0))
  107. }, 1000)
  108. return () => clearInterval(interval)
  109. }, [cancelled, opened])
  110. // Navigate to sign-in page when countdown hits 0
  111. useEffect(() => {
  112. if (countdown > 0) return
  113. navigate({ to: '/clerk/sign-in' })
  114. }, [countdown, navigate])
  115. return (
  116. <div className='h-svh'>
  117. <div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
  118. <h1 className='text-[7rem] leading-tight font-bold'>401</h1>
  119. <span className='font-medium'>Unauthorized Access</span>
  120. <p className='text-muted-foreground text-center'>
  121. You must be authenticated via Clerk{' '}
  122. <sup>
  123. <LearnMore open={opened} onOpenChange={setOpened}>
  124. <p>
  125. This is the same as{' '}
  126. <Link
  127. to='/users'
  128. className='text-blue-500 underline decoration-dashed underline-offset-2'
  129. >
  130. '/users'
  131. </Link>
  132. .{' '}
  133. </p>
  134. <p>You must first sign in using Clerk to access this route. </p>
  135. <p className='mt-4'>
  136. After signing in, you'll be able to sign out or delete your
  137. account via the User Profile dropdown on this page.
  138. </p>
  139. </LearnMore>
  140. </sup>
  141. <br />
  142. to access this resource.
  143. </p>
  144. <div className='mt-6 flex gap-4'>
  145. <Button variant='outline' onClick={() => history.go(-1)}>
  146. Go Back
  147. </Button>
  148. <Button onClick={() => navigate({ to: '/clerk/sign-in' })}>
  149. <ClerkLogo className='invert' /> Sign in
  150. </Button>
  151. </div>
  152. <div className='mt-4 h-8 text-center'>
  153. {!cancelled && !opened && (
  154. <>
  155. <p>
  156. {countdown > 0
  157. ? `Redirecting to Sign In page in ${countdown}s`
  158. : `Redirecting...`}
  159. </p>
  160. <Button variant='link' onClick={() => setCancelled(true)}>
  161. Cancel Redirect
  162. </Button>
  163. </>
  164. )}
  165. </div>
  166. </div>
  167. </div>
  168. )
  169. }