Răsfoiți Sursa

feat(clerk): add Clerk for auth and protected route (#146)

* feat: implement clerk auth

* refactor: extract authenticated route layout

* fix: add signUpFallbackRedirectUrl in ClerkProvider

* fix: add default email address in clerk sign in

* feat: add user-management flow with Clerk auth

* refactor: extract learn-more component

* chore: add learn more button in clerk auth layout

* fix: update nav title for Clerk

* fix: add example env file

* feat(clerk): add fallback UI when publishable key is missing

* docs(clerk): add clerk in readme
Sat Naing 1 an în urmă
părinte
comite
be3d7d884d

+ 1 - 0
.env.example

@@ -0,0 +1 @@
+VITE_CLERK_PUBLISHABLE_KEY=

+ 2 - 0
.gitignore

@@ -12,6 +12,8 @@ dist
 dist-ssr
 *.local
 
+.env
+
 # Editor directories and files
 .vscode/*
 !.vscode/extensions.json

+ 12 - 0
README.md

@@ -32,6 +32,8 @@ I've been creating dashboard UIs at work and for my personal projects. I always
 
 **Icons:** [Tabler Icons](https://tabler.io/icons)
 
+**Auth (partial):** [Clerk](https://go.clerk.com/GttUAaK)
+
 ## Run Locally
 
 Clone the project
@@ -58,6 +60,16 @@ Start the server
   pnpm run dev
 ```
 
+## Sponsoring this project ❤️
+
+If you find this project helpful or use this in your own work, consider [sponsoring me](https://github.com/sponsors/satnaing) to support development and maintenance. You can [buy me a coffee](https://buymeacoffee.com/satnaing) as well. Don’t worry, every penny helps. Thank you! 🙏
+
+For questions or sponsorship inquiries, feel free to reach out at [contact@satnaing.dev](mailto:contact@satnaing.dev).
+
+### Current Sponsor
+
+- [Clerk](https://go.clerk.com/GttUAaK) - for backing the implementation of Clerk in this project
+
 ## Author
 
 Crafted with 🤍 by [@satnaing](https://github.com/satnaing)

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "knip": "knip"
   },
   "dependencies": {
+    "@clerk/clerk-react": "^5.31.4",
     "@hookform/resolvers": "^5.0.1",
     "@radix-ui/react-alert-dialog": "^1.1.7",
     "@radix-ui/react-avatar": "^1.1.4",

+ 77 - 0
pnpm-lock.yaml

@@ -8,6 +8,9 @@ importers:
 
   .:
     dependencies:
+      '@clerk/clerk-react':
+        specifier: ^5.31.4
+        version: 5.31.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
       '@hookform/resolvers':
         specifier: ^5.0.1
         version: 5.0.1(react-hook-form@7.55.0(react@19.1.0))
@@ -294,6 +297,29 @@ packages:
     resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==}
     engines: {node: '>=6.9.0'}
 
+  '@clerk/clerk-react@5.31.4':
+    resolution: {integrity: sha512-VtjOEzq/ncwHRn23xhmy4DRefrrSeUkKHiB/EighusYVkjmpzWMXYGD9Wdd79hwUhJesUsBiQdZhE5qkJ+mnJA==}
+    engines: {node: '>=18.17.0'}
+    peerDependencies:
+      react: ^18.0.0 || ^19.0.0 || ^19.0.0-0
+      react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0
+
+  '@clerk/shared@3.9.1':
+    resolution: {integrity: sha512-Gw7yPaas3lv+pkkbBwuqVVtWVH1nZl1hF8kVvdEhPALOkf6ww6azL6qcHaiFUHCW+iult3flJjplduFrfWF50g==}
+    engines: {node: '>=18.17.0'}
+    peerDependencies:
+      react: ^18.0.0 || ^19.0.0 || ^19.0.0-0
+      react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0
+    peerDependenciesMeta:
+      react:
+        optional: true
+      react-dom:
+        optional: true
+
+  '@clerk/types@4.59.0':
+    resolution: {integrity: sha512-VZ61lDWoz9cWTlSpO1KMGq7utl96ZuSBIOpM6togxYTp+TG0kD6QEJVinMaJLREtx8jRvXpMG7ZzBLE3zy0GSA==}
+    engines: {node: '>=18.17.0'}
+
   '@date-fns/tz@1.2.0':
     resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
 
@@ -1761,6 +1787,10 @@ packages:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
 
+  dequal@2.0.3:
+    resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+    engines: {node: '>=6'}
+
   detect-libc@2.0.3:
     resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
     engines: {node: '>=8'}
@@ -1961,6 +1991,9 @@ packages:
     resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
     engines: {node: '>=10.13.0'}
 
+  glob-to-regexp@0.4.1:
+    resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
+
   globals@11.12.0:
     resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
     engines: {node: '>=4'}
@@ -2517,6 +2550,9 @@ packages:
     resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
     engines: {node: '>=0.10.0'}
 
+  std-env@3.9.0:
+    resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
+
   strip-json-comments@3.1.1:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
@@ -2529,6 +2565,11 @@ packages:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
     engines: {node: '>=8'}
 
+  swr@2.3.3:
+    resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==}
+    peerDependencies:
+      react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
   tailwind-merge@3.2.0:
     resolution: {integrity: sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==}
 
@@ -2836,6 +2877,30 @@ snapshots:
       '@babel/helper-string-parser': 7.25.9
       '@babel/helper-validator-identifier': 7.25.9
 
+  '@clerk/clerk-react@5.31.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+    dependencies:
+      '@clerk/shared': 3.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      '@clerk/types': 4.59.0
+      react: 19.1.0
+      react-dom: 19.1.0(react@19.1.0)
+      tslib: 2.8.1
+
+  '@clerk/shared@3.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+    dependencies:
+      '@clerk/types': 4.59.0
+      dequal: 2.0.3
+      glob-to-regexp: 0.4.1
+      js-cookie: 3.0.5
+      std-env: 3.9.0
+      swr: 2.3.3(react@19.1.0)
+    optionalDependencies:
+      react: 19.1.0
+      react-dom: 19.1.0(react@19.1.0)
+
+  '@clerk/types@4.59.0':
+    dependencies:
+      csstype: 3.1.3
+
   '@date-fns/tz@1.2.0': {}
 
   '@esbuild/aix-ppc64@0.25.3':
@@ -4200,6 +4265,8 @@ snapshots:
 
   delayed-stream@1.0.0: {}
 
+  dequal@2.0.3: {}
+
   detect-libc@2.0.3: {}
 
   detect-node-es@1.1.0: {}
@@ -4444,6 +4511,8 @@ snapshots:
     dependencies:
       is-glob: 4.0.3
 
+  glob-to-regexp@0.4.1: {}
+
   globals@11.12.0: {}
 
   globals@14.0.0: {}
@@ -4881,6 +4950,8 @@ snapshots:
 
   source-map-js@1.2.1: {}
 
+  std-env@3.9.0: {}
+
   strip-json-comments@3.1.1: {}
 
   strip-json-comments@5.0.1: {}
@@ -4889,6 +4960,12 @@ snapshots:
     dependencies:
       has-flag: 4.0.0
 
+  swr@2.3.3(react@19.1.0):
+    dependencies:
+      dequal: 2.0.3
+      react: 19.1.0
+      use-sync-external-store: 1.5.0(react@19.1.0)
+
   tailwind-merge@3.2.0: {}
 
   tailwindcss@4.1.4: {}

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

@@ -0,0 +1,41 @@
+import { 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>
+  )
+}

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

@@ -0,0 +1,23 @@
+import { 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>
+  )
+}

+ 37 - 0
src/components/layout/authenticated-layout.tsx

@@ -0,0 +1,37 @@
+import Cookies from 'js-cookie'
+import { Outlet } from '@tanstack/react-router'
+import { cn } from '@/lib/utils'
+import { SearchProvider } from '@/context/search-context'
+import { SidebarProvider } from '@/components/ui/sidebar'
+import { AppSidebar } from '@/components/layout/app-sidebar'
+import SkipToMain from '@/components/skip-to-main'
+
+interface Props {
+  children?: React.ReactNode
+}
+
+export function AuthenticatedLayout({ children }: Props) {
+  const defaultOpen = Cookies.get('sidebar_state') !== 'false'
+  return (
+    <SearchProvider>
+      <SidebarProvider defaultOpen={defaultOpen}>
+        <SkipToMain />
+        <AppSidebar />
+        <div
+          id='content'
+          className={cn(
+            'ml-auto w-full max-w-full',
+            'peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon)-1rem)]',
+            'peer-data-[state=expanded]:w-[calc(100%-var(--sidebar-width))]',
+            'sm:transition-[width] sm:duration-200 sm:ease-linear',
+            'flex h-svh flex-col',
+            'group-data-[scroll-locked=1]/body:h-full',
+            'has-[main.fixed-main]:group-data-[scroll-locked=1]/body:h-svh'
+          )}
+        >
+          {children ? children : <Outlet />}
+        </div>
+      </SidebarProvider>
+    </SearchProvider>
+  )
+}

+ 19 - 0
src/components/layout/data/sidebar-data.ts

@@ -20,6 +20,7 @@ import {
   IconUsers,
 } from '@tabler/icons-react'
 import { AudioWaveform, Command, GalleryVerticalEnd } from 'lucide-react'
+import { ClerkLogo } from '@/assets/clerk-logo'
 import { type SidebarData } from '../types'
 
 export const sidebarData: SidebarData = {
@@ -75,6 +76,24 @@ export const sidebarData: SidebarData = {
           url: '/users',
           icon: IconUsers,
         },
+        {
+          title: 'Secured by Clerk',
+          icon: ClerkLogo,
+          items: [
+            {
+              title: 'Sign In',
+              url: '/clerk/sign-in',
+            },
+            {
+              title: 'Sign Up',
+              url: '/clerk/sign-up',
+            },
+            {
+              title: 'User Management',
+              url: '/clerk/user-management',
+            },
+          ],
+        },
       ],
     },
     {

+ 3 - 2
src/components/layout/main.tsx

@@ -6,13 +6,14 @@ interface MainProps extends React.HTMLAttributes<HTMLElement> {
   ref?: React.Ref<HTMLElement>
 }
 
-export const Main = ({ fixed, ...props }: MainProps) => {
+export const Main = ({ fixed, className, ...props }: MainProps) => {
   return (
     <main
       className={cn(
         'peer-[.header-fixed]/header:mt-16',
         'px-4 py-6',
-        fixed && 'fixed-main flex grow flex-col overflow-hidden'
+        fixed && 'fixed-main flex grow flex-col overflow-hidden',
+        className
       )}
       {...props}
     />

+ 44 - 0
src/components/learn-more.tsx

@@ -0,0 +1,44 @@
+import { Root, Content, Trigger } from '@radix-ui/react-popover'
+import { IconQuestionMark } from '@tabler/icons-react'
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from '@/components/ui/popover'
+
+interface Props extends React.ComponentProps<typeof Root> {
+  contentProps?: React.ComponentProps<typeof Content>
+  triggerProps?: React.ComponentProps<typeof Trigger>
+}
+
+export function LearnMore({
+  children,
+  contentProps,
+  triggerProps,
+  ...props
+}: Props) {
+  return (
+    <Popover {...props}>
+      <PopoverTrigger
+        asChild
+        {...triggerProps}
+        className={cn('size-5 rounded-full', triggerProps?.className)}
+      >
+        <Button variant='outline' size='icon'>
+          <span className='sr-only'>Learn more</span>
+          <IconQuestionMark className='size-3' />
+        </Button>
+      </PopoverTrigger>
+      <PopoverContent
+        side='top'
+        align='start'
+        {...contentProps}
+        className={cn('text-muted-foreground text-sm', contentProps?.className)}
+      >
+        {children}
+      </PopoverContent>
+    </Popover>
+  )
+}

+ 193 - 0
src/routeTree.gen.ts

@@ -11,6 +11,7 @@
 // Import Routes
 
 import { Route as rootRoute } from './routes/__root'
+import { Route as ClerkRouteImport } from './routes/clerk/route'
 import { Route as AuthenticatedRouteImport } from './routes/_authenticated/route'
 import { Route as AuthenticatedIndexImport } from './routes/_authenticated/index'
 import { Route as errors503Import } from './routes/(errors)/503'
@@ -23,6 +24,8 @@ import { Route as authSignIn2Import } from './routes/(auth)/sign-in-2'
 import { Route as authSignInImport } from './routes/(auth)/sign-in'
 import { Route as authOtpImport } from './routes/(auth)/otp'
 import { Route as authForgotPasswordImport } from './routes/(auth)/forgot-password'
+import { Route as ClerkAuthenticatedRouteImport } from './routes/clerk/_authenticated/route'
+import { Route as ClerkauthRouteImport } from './routes/clerk/(auth)/route'
 import { Route as AuthenticatedSettingsRouteImport } from './routes/_authenticated/settings/route'
 import { Route as AuthenticatedUsersIndexImport } from './routes/_authenticated/users/index'
 import { Route as AuthenticatedTasksIndexImport } from './routes/_authenticated/tasks/index'
@@ -30,6 +33,9 @@ import { Route as AuthenticatedSettingsIndexImport } from './routes/_authenticat
 import { Route as AuthenticatedHelpCenterIndexImport } from './routes/_authenticated/help-center/index'
 import { Route as AuthenticatedChatsIndexImport } from './routes/_authenticated/chats/index'
 import { Route as AuthenticatedAppsIndexImport } from './routes/_authenticated/apps/index'
+import { Route as ClerkAuthenticatedUserManagementImport } from './routes/clerk/_authenticated/user-management'
+import { Route as ClerkauthSignUpImport } from './routes/clerk/(auth)/sign-up'
+import { Route as ClerkauthSignInImport } from './routes/clerk/(auth)/sign-in'
 import { Route as AuthenticatedSettingsNotificationsImport } from './routes/_authenticated/settings/notifications'
 import { Route as AuthenticatedSettingsDisplayImport } from './routes/_authenticated/settings/display'
 import { Route as AuthenticatedSettingsAppearanceImport } from './routes/_authenticated/settings/appearance'
@@ -37,6 +43,12 @@ import { Route as AuthenticatedSettingsAccountImport } from './routes/_authentic
 
 // Create/Update Routes
 
+const ClerkRouteRoute = ClerkRouteImport.update({
+  id: '/clerk',
+  path: '/clerk',
+  getParentRoute: () => rootRoute,
+} as any)
+
 const AuthenticatedRouteRoute = AuthenticatedRouteImport.update({
   id: '/_authenticated',
   getParentRoute: () => rootRoute,
@@ -108,6 +120,16 @@ const authForgotPasswordRoute = authForgotPasswordImport.update({
   getParentRoute: () => rootRoute,
 } as any)
 
+const ClerkAuthenticatedRouteRoute = ClerkAuthenticatedRouteImport.update({
+  id: '/_authenticated',
+  getParentRoute: () => ClerkRouteRoute,
+} as any)
+
+const ClerkauthRouteRoute = ClerkauthRouteImport.update({
+  id: '/(auth)',
+  getParentRoute: () => ClerkRouteRoute,
+} as any)
+
 const AuthenticatedSettingsRouteRoute = AuthenticatedSettingsRouteImport.update(
   {
     id: '/settings',
@@ -155,6 +177,25 @@ const AuthenticatedAppsIndexRoute = AuthenticatedAppsIndexImport.update({
   getParentRoute: () => AuthenticatedRouteRoute,
 } as any)
 
+const ClerkAuthenticatedUserManagementRoute =
+  ClerkAuthenticatedUserManagementImport.update({
+    id: '/user-management',
+    path: '/user-management',
+    getParentRoute: () => ClerkAuthenticatedRouteRoute,
+  } as any)
+
+const ClerkauthSignUpRoute = ClerkauthSignUpImport.update({
+  id: '/sign-up',
+  path: '/sign-up',
+  getParentRoute: () => ClerkauthRouteRoute,
+} as any)
+
+const ClerkauthSignInRoute = ClerkauthSignInImport.update({
+  id: '/sign-in',
+  path: '/sign-in',
+  getParentRoute: () => ClerkauthRouteRoute,
+} as any)
+
 const AuthenticatedSettingsNotificationsRoute =
   AuthenticatedSettingsNotificationsImport.update({
     id: '/notifications',
@@ -194,6 +235,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedRouteImport
       parentRoute: typeof rootRoute
     }
+    '/clerk': {
+      id: '/clerk'
+      path: '/clerk'
+      fullPath: '/clerk'
+      preLoaderRoute: typeof ClerkRouteImport
+      parentRoute: typeof rootRoute
+    }
     '/_authenticated/settings': {
       id: '/_authenticated/settings'
       path: '/settings'
@@ -201,6 +249,20 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSettingsRouteImport
       parentRoute: typeof AuthenticatedRouteImport
     }
+    '/clerk/(auth)': {
+      id: '/clerk/(auth)'
+      path: '/'
+      fullPath: '/clerk/'
+      preLoaderRoute: typeof ClerkauthRouteImport
+      parentRoute: typeof ClerkRouteImport
+    }
+    '/clerk/_authenticated': {
+      id: '/clerk/_authenticated'
+      path: ''
+      fullPath: '/clerk'
+      preLoaderRoute: typeof ClerkAuthenticatedRouteImport
+      parentRoute: typeof ClerkRouteImport
+    }
     '/(auth)/forgot-password': {
       id: '/(auth)/forgot-password'
       path: '/forgot-password'
@@ -306,6 +368,27 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSettingsNotificationsImport
       parentRoute: typeof AuthenticatedSettingsRouteImport
     }
+    '/clerk/(auth)/sign-in': {
+      id: '/clerk/(auth)/sign-in'
+      path: '/sign-in'
+      fullPath: '/clerk/sign-in'
+      preLoaderRoute: typeof ClerkauthSignInImport
+      parentRoute: typeof ClerkauthRouteImport
+    }
+    '/clerk/(auth)/sign-up': {
+      id: '/clerk/(auth)/sign-up'
+      path: '/sign-up'
+      fullPath: '/clerk/sign-up'
+      preLoaderRoute: typeof ClerkauthSignUpImport
+      parentRoute: typeof ClerkauthRouteImport
+    }
+    '/clerk/_authenticated/user-management': {
+      id: '/clerk/_authenticated/user-management'
+      path: '/user-management'
+      fullPath: '/clerk/user-management'
+      preLoaderRoute: typeof ClerkAuthenticatedUserManagementImport
+      parentRoute: typeof ClerkAuthenticatedRouteImport
+    }
     '/_authenticated/apps/': {
       id: '/_authenticated/apps/'
       path: '/apps'
@@ -399,9 +482,54 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
 const AuthenticatedRouteRouteWithChildren =
   AuthenticatedRouteRoute._addFileChildren(AuthenticatedRouteRouteChildren)
 
+interface ClerkauthRouteRouteChildren {
+  ClerkauthSignInRoute: typeof ClerkauthSignInRoute
+  ClerkauthSignUpRoute: typeof ClerkauthSignUpRoute
+}
+
+const ClerkauthRouteRouteChildren: ClerkauthRouteRouteChildren = {
+  ClerkauthSignInRoute: ClerkauthSignInRoute,
+  ClerkauthSignUpRoute: ClerkauthSignUpRoute,
+}
+
+const ClerkauthRouteRouteWithChildren = ClerkauthRouteRoute._addFileChildren(
+  ClerkauthRouteRouteChildren,
+)
+
+interface ClerkAuthenticatedRouteRouteChildren {
+  ClerkAuthenticatedUserManagementRoute: typeof ClerkAuthenticatedUserManagementRoute
+}
+
+const ClerkAuthenticatedRouteRouteChildren: ClerkAuthenticatedRouteRouteChildren =
+  {
+    ClerkAuthenticatedUserManagementRoute:
+      ClerkAuthenticatedUserManagementRoute,
+  }
+
+const ClerkAuthenticatedRouteRouteWithChildren =
+  ClerkAuthenticatedRouteRoute._addFileChildren(
+    ClerkAuthenticatedRouteRouteChildren,
+  )
+
+interface ClerkRouteRouteChildren {
+  ClerkauthRouteRoute: typeof ClerkauthRouteRouteWithChildren
+  ClerkAuthenticatedRouteRoute: typeof ClerkAuthenticatedRouteRouteWithChildren
+}
+
+const ClerkRouteRouteChildren: ClerkRouteRouteChildren = {
+  ClerkauthRouteRoute: ClerkauthRouteRouteWithChildren,
+  ClerkAuthenticatedRouteRoute: ClerkAuthenticatedRouteRouteWithChildren,
+}
+
+const ClerkRouteRouteWithChildren = ClerkRouteRoute._addFileChildren(
+  ClerkRouteRouteChildren,
+)
+
 export interface FileRoutesByFullPath {
   '': typeof AuthenticatedRouteRouteWithChildren
+  '/clerk': typeof ClerkAuthenticatedRouteRouteWithChildren
   '/settings': typeof AuthenticatedSettingsRouteRouteWithChildren
+  '/clerk/': typeof ClerkauthRouteRouteWithChildren
   '/forgot-password': typeof authForgotPasswordRoute
   '/otp': typeof authOtpRoute
   '/sign-in': typeof authSignInRoute
@@ -417,6 +545,9 @@ export interface FileRoutesByFullPath {
   '/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
   '/settings/display': typeof AuthenticatedSettingsDisplayRoute
   '/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute
+  '/clerk/sign-in': typeof ClerkauthSignInRoute
+  '/clerk/sign-up': typeof ClerkauthSignUpRoute
+  '/clerk/user-management': typeof ClerkAuthenticatedUserManagementRoute
   '/apps': typeof AuthenticatedAppsIndexRoute
   '/chats': typeof AuthenticatedChatsIndexRoute
   '/help-center': typeof AuthenticatedHelpCenterIndexRoute
@@ -426,6 +557,7 @@ export interface FileRoutesByFullPath {
 }
 
 export interface FileRoutesByTo {
+  '/clerk': typeof ClerkAuthenticatedRouteRouteWithChildren
   '/forgot-password': typeof authForgotPasswordRoute
   '/otp': typeof authOtpRoute
   '/sign-in': typeof authSignInRoute
@@ -441,6 +573,9 @@ export interface FileRoutesByTo {
   '/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
   '/settings/display': typeof AuthenticatedSettingsDisplayRoute
   '/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute
+  '/clerk/sign-in': typeof ClerkauthSignInRoute
+  '/clerk/sign-up': typeof ClerkauthSignUpRoute
+  '/clerk/user-management': typeof ClerkAuthenticatedUserManagementRoute
   '/apps': typeof AuthenticatedAppsIndexRoute
   '/chats': typeof AuthenticatedChatsIndexRoute
   '/help-center': typeof AuthenticatedHelpCenterIndexRoute
@@ -452,7 +587,10 @@ export interface FileRoutesByTo {
 export interface FileRoutesById {
   __root__: typeof rootRoute
   '/_authenticated': typeof AuthenticatedRouteRouteWithChildren
+  '/clerk': typeof ClerkRouteRouteWithChildren
   '/_authenticated/settings': typeof AuthenticatedSettingsRouteRouteWithChildren
+  '/clerk/(auth)': typeof ClerkauthRouteRouteWithChildren
+  '/clerk/_authenticated': typeof ClerkAuthenticatedRouteRouteWithChildren
   '/(auth)/forgot-password': typeof authForgotPasswordRoute
   '/(auth)/otp': typeof authOtpRoute
   '/(auth)/sign-in': typeof authSignInRoute
@@ -468,6 +606,9 @@ export interface FileRoutesById {
   '/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
   '/_authenticated/settings/display': typeof AuthenticatedSettingsDisplayRoute
   '/_authenticated/settings/notifications': typeof AuthenticatedSettingsNotificationsRoute
+  '/clerk/(auth)/sign-in': typeof ClerkauthSignInRoute
+  '/clerk/(auth)/sign-up': typeof ClerkauthSignUpRoute
+  '/clerk/_authenticated/user-management': typeof ClerkAuthenticatedUserManagementRoute
   '/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute
   '/_authenticated/chats/': typeof AuthenticatedChatsIndexRoute
   '/_authenticated/help-center/': typeof AuthenticatedHelpCenterIndexRoute
@@ -480,7 +621,9 @@ export interface FileRouteTypes {
   fileRoutesByFullPath: FileRoutesByFullPath
   fullPaths:
     | ''
+    | '/clerk'
     | '/settings'
+    | '/clerk/'
     | '/forgot-password'
     | '/otp'
     | '/sign-in'
@@ -496,6 +639,9 @@ export interface FileRouteTypes {
     | '/settings/appearance'
     | '/settings/display'
     | '/settings/notifications'
+    | '/clerk/sign-in'
+    | '/clerk/sign-up'
+    | '/clerk/user-management'
     | '/apps'
     | '/chats'
     | '/help-center'
@@ -504,6 +650,7 @@ export interface FileRouteTypes {
     | '/users'
   fileRoutesByTo: FileRoutesByTo
   to:
+    | '/clerk'
     | '/forgot-password'
     | '/otp'
     | '/sign-in'
@@ -519,6 +666,9 @@ export interface FileRouteTypes {
     | '/settings/appearance'
     | '/settings/display'
     | '/settings/notifications'
+    | '/clerk/sign-in'
+    | '/clerk/sign-up'
+    | '/clerk/user-management'
     | '/apps'
     | '/chats'
     | '/help-center'
@@ -528,7 +678,10 @@ export interface FileRouteTypes {
   id:
     | '__root__'
     | '/_authenticated'
+    | '/clerk'
     | '/_authenticated/settings'
+    | '/clerk/(auth)'
+    | '/clerk/_authenticated'
     | '/(auth)/forgot-password'
     | '/(auth)/otp'
     | '/(auth)/sign-in'
@@ -544,6 +697,9 @@ export interface FileRouteTypes {
     | '/_authenticated/settings/appearance'
     | '/_authenticated/settings/display'
     | '/_authenticated/settings/notifications'
+    | '/clerk/(auth)/sign-in'
+    | '/clerk/(auth)/sign-up'
+    | '/clerk/_authenticated/user-management'
     | '/_authenticated/apps/'
     | '/_authenticated/chats/'
     | '/_authenticated/help-center/'
@@ -555,6 +711,7 @@ export interface FileRouteTypes {
 
 export interface RootRouteChildren {
   AuthenticatedRouteRoute: typeof AuthenticatedRouteRouteWithChildren
+  ClerkRouteRoute: typeof ClerkRouteRouteWithChildren
   authForgotPasswordRoute: typeof authForgotPasswordRoute
   authOtpRoute: typeof authOtpRoute
   authSignInRoute: typeof authSignInRoute
@@ -569,6 +726,7 @@ export interface RootRouteChildren {
 
 const rootRouteChildren: RootRouteChildren = {
   AuthenticatedRouteRoute: AuthenticatedRouteRouteWithChildren,
+  ClerkRouteRoute: ClerkRouteRouteWithChildren,
   authForgotPasswordRoute: authForgotPasswordRoute,
   authOtpRoute: authOtpRoute,
   authSignInRoute: authSignInRoute,
@@ -592,6 +750,7 @@ export const routeTree = rootRoute
       "filePath": "__root.tsx",
       "children": [
         "/_authenticated",
+        "/clerk",
         "/(auth)/forgot-password",
         "/(auth)/otp",
         "/(auth)/sign-in",
@@ -616,6 +775,13 @@ export const routeTree = rootRoute
         "/_authenticated/users/"
       ]
     },
+    "/clerk": {
+      "filePath": "clerk/route.tsx",
+      "children": [
+        "/clerk/(auth)",
+        "/clerk/_authenticated"
+      ]
+    },
     "/_authenticated/settings": {
       "filePath": "_authenticated/settings/route.tsx",
       "parent": "/_authenticated",
@@ -627,6 +793,21 @@ export const routeTree = rootRoute
         "/_authenticated/settings/"
       ]
     },
+    "/clerk/(auth)": {
+      "filePath": "clerk/(auth)/route.tsx",
+      "parent": "/clerk",
+      "children": [
+        "/clerk/(auth)/sign-in",
+        "/clerk/(auth)/sign-up"
+      ]
+    },
+    "/clerk/_authenticated": {
+      "filePath": "clerk/_authenticated/route.tsx",
+      "parent": "/clerk",
+      "children": [
+        "/clerk/_authenticated/user-management"
+      ]
+    },
     "/(auth)/forgot-password": {
       "filePath": "(auth)/forgot-password.tsx"
     },
@@ -677,6 +858,18 @@ export const routeTree = rootRoute
       "filePath": "_authenticated/settings/notifications.tsx",
       "parent": "/_authenticated/settings"
     },
+    "/clerk/(auth)/sign-in": {
+      "filePath": "clerk/(auth)/sign-in.tsx",
+      "parent": "/clerk/(auth)"
+    },
+    "/clerk/(auth)/sign-up": {
+      "filePath": "clerk/(auth)/sign-up.tsx",
+      "parent": "/clerk/(auth)"
+    },
+    "/clerk/_authenticated/user-management": {
+      "filePath": "clerk/_authenticated/user-management.tsx",
+      "parent": "/clerk/_authenticated"
+    },
     "/_authenticated/apps/": {
       "filePath": "_authenticated/apps/index.tsx",
       "parent": "/_authenticated"

+ 3 - 34
src/routes/_authenticated/route.tsx

@@ -1,37 +1,6 @@
-import Cookies from 'js-cookie'
-import { createFileRoute, Outlet } from '@tanstack/react-router'
-import { cn } from '@/lib/utils'
-import { SearchProvider } from '@/context/search-context'
-import { SidebarProvider } from '@/components/ui/sidebar'
-import { AppSidebar } from '@/components/layout/app-sidebar'
-import SkipToMain from '@/components/skip-to-main'
+import { createFileRoute } from '@tanstack/react-router'
+import { AuthenticatedLayout } from '@/components/layout/authenticated-layout'
 
 export const Route = createFileRoute('/_authenticated')({
-  component: RouteComponent,
+  component: AuthenticatedLayout,
 })
-
-function RouteComponent() {
-  const defaultOpen = Cookies.get('sidebar_state') !== 'false'
-  return (
-    <SearchProvider>
-      <SidebarProvider defaultOpen={defaultOpen}>
-        <SkipToMain />
-        <AppSidebar />
-        <div
-          id='content'
-          className={cn(
-            'ml-auto w-full max-w-full',
-            'peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon)-1rem)]',
-            'peer-data-[state=expanded]:w-[calc(100%-var(--sidebar-width))]',
-            'sm:transition-[width] sm:duration-200 sm:ease-linear',
-            'flex h-svh flex-col',
-            'group-data-[scroll-locked=1]/body:h-full',
-            'has-[main.fixed-main]:group-data-[scroll-locked=1]/body:h-svh'
-          )}
-        >
-          <Outlet />
-        </div>
-      </SidebarProvider>
-    </SearchProvider>
-  )
-}

+ 69 - 0
src/routes/clerk/(auth)/route.tsx

@@ -0,0 +1,69 @@
+import { createFileRoute, Link, Outlet } from '@tanstack/react-router'
+import { ClerkFullLogo } from '@/assets/clerk-full-logo'
+import { LearnMore } from '@/components/learn-more'
+
+export const Route = createFileRoute('/clerk/(auth)')({
+  component: ClerkAuthLayout,
+})
+
+function ClerkAuthLayout() {
+  return (
+    <div className='relative container grid h-svh flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0'>
+      <div className='bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-r'>
+        <div className='absolute inset-0 bg-slate-500' />
+        <Link
+          to='/'
+          className='relative z-20 flex items-center text-lg font-medium'
+        >
+          <svg
+            xmlns='http://www.w3.org/2000/svg'
+            viewBox='0 0 24 24'
+            fill='none'
+            stroke='currentColor'
+            strokeWidth='2'
+            strokeLinecap='round'
+            strokeLinejoin='round'
+            className='mr-2 h-6 w-6'
+          >
+            <path d='M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3' />
+          </svg>
+          Shadcn Admin
+        </Link>
+
+        <ClerkFullLogo className='relative m-auto size-96' />
+
+        <div className='relative z-20 mt-auto'>
+          <blockquote className='space-y-2'>
+            <p className='text-lg'>
+              &ldquo; Lorem ipsum dolor sit amet consectetur adipisicing elit.
+              Sint, magni debitis inventore asperiores velit! &rdquo;
+            </p>
+            <footer className='text-sm'>John Doe</footer>
+          </blockquote>
+        </div>
+      </div>
+      <div className='lg:p-8'>
+        <div className='relative mx-auto flex w-full flex-col items-center justify-center gap-4'>
+          <LearnMore
+            defaultOpen
+            triggerProps={{
+              className: 'absolute -top-12 right-0 sm:right-20 size-6',
+            }}
+            contentProps={{ side: 'top', align: 'end', className: 'w-auto' }}
+          >
+            Welcome to the example Clerk auth page. <br />
+            Back to{' '}
+            <Link
+              to='/'
+              className='underline decoration-dashed underline-offset-2'
+            >
+              Dashboard
+            </Link>{' '}
+            ?
+          </LearnMore>
+          <Outlet />
+        </div>
+      </div>
+    </div>
+  )
+}

+ 14 - 0
src/routes/clerk/(auth)/sign-in.tsx

@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { SignIn } from '@clerk/clerk-react'
+import { Skeleton } from '@/components/ui/skeleton'
+
+export const Route = createFileRoute('/clerk/(auth)/sign-in')({
+  component: () => (
+    <SignIn
+      initialValues={{
+        emailAddress: 'your_mail+shadcn_admin@gmail.com',
+      }}
+      fallback={<Skeleton className='h-[30rem] w-[25rem]' />}
+    />
+  ),
+})

+ 9 - 0
src/routes/clerk/(auth)/sign-up.tsx

@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { SignUp } from '@clerk/clerk-react'
+import { Skeleton } from '@/components/ui/skeleton'
+
+export const Route = createFileRoute('/clerk/(auth)/sign-up')({
+  component: () => (
+    <SignUp fallback={<Skeleton className='h-[30rem] w-[25rem]' />} />
+  ),
+})

+ 6 - 0
src/routes/clerk/_authenticated/route.tsx

@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { AuthenticatedLayout } from '@/components/layout/authenticated-layout'
+
+export const Route = createFileRoute('/clerk/_authenticated')({
+  component: AuthenticatedLayout,
+})

+ 184 - 0
src/routes/clerk/_authenticated/user-management.tsx

@@ -0,0 +1,184 @@
+import { useEffect, useState } from 'react'
+import {
+  createFileRoute,
+  Link,
+  useNavigate,
+  useRouter,
+} from '@tanstack/react-router'
+import { IconArrowUpRight, IconLoader2 } from '@tabler/icons-react'
+import { SignedIn, useAuth, UserButton } from '@clerk/clerk-react'
+import { ClerkLogo } from '@/assets/clerk-logo'
+import { Button } from '@/components/ui/button'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
+import { LearnMore } from '@/components/learn-more'
+import { Search } from '@/components/search'
+import { ThemeSwitch } from '@/components/theme-switch'
+import { columns } from '@/features/users/components/users-columns'
+import { UsersDialogs } from '@/features/users/components/users-dialogs'
+import { UsersPrimaryButtons } from '@/features/users/components/users-primary-buttons'
+import { UsersTable } from '@/features/users/components/users-table'
+import UsersProvider from '@/features/users/context/users-context'
+import { userListSchema } from '@/features/users/data/schema'
+import { users } from '@/features/users/data/users'
+
+export const Route = createFileRoute('/clerk/_authenticated/user-management')({
+  component: UserManagement,
+})
+
+function UserManagement() {
+  const [opened, setOpened] = useState(true)
+  const { isLoaded, isSignedIn } = useAuth()
+
+  if (!isLoaded) {
+    return (
+      <div className='flex h-svh items-center justify-center'>
+        <IconLoader2 className='size-8 animate-spin' />
+      </div>
+    )
+  }
+
+  if (!isSignedIn) {
+    return <Unauthorized />
+  }
+
+  // Parse user list
+  const userList = userListSchema.parse(users)
+  return (
+    <>
+      <SignedIn>
+        <UsersProvider>
+          <Header fixed>
+            <Search />
+            <div className='ml-auto flex items-center space-x-4'>
+              <ThemeSwitch />
+              <UserButton />
+            </div>
+          </Header>
+
+          <Main>
+            <div className='mb-2 flex flex-wrap items-center justify-between space-y-2'>
+              <div>
+                <h2 className='text-2xl font-bold tracking-tight'>User List</h2>
+                <div className='flex gap-1'>
+                  <p className='text-muted-foreground'>
+                    Manage your users and their roles here.
+                  </p>
+                  <LearnMore
+                    open={opened}
+                    onOpenChange={setOpened}
+                    contentProps={{ side: 'right' }}
+                  >
+                    <p>
+                      This is the same as{' '}
+                      <Link
+                        to='/users'
+                        className='text-blue-500 underline decoration-dashed underline-offset-2'
+                      >
+                        '/users'
+                      </Link>
+                    </p>
+
+                    <p className='mt-4'>
+                      You can sign out or manage/delete your account via the
+                      User Profile menu in the top-right corner of the page.
+                      <IconArrowUpRight className='inline-block size-4' />
+                    </p>
+                  </LearnMore>
+                </div>
+              </div>
+              <UsersPrimaryButtons />
+            </div>
+            <div className='-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-y-0 lg:space-x-12'>
+              <UsersTable data={userList} columns={columns} />
+            </div>
+          </Main>
+
+          <UsersDialogs />
+        </UsersProvider>
+      </SignedIn>
+    </>
+  )
+}
+
+const COUNTDOWN = 5 // Countdown second
+
+function Unauthorized() {
+  const navigate = useNavigate()
+  const { history } = useRouter()
+
+  const [opened, setOpened] = useState(true)
+  const [cancelled, setCancelled] = useState(false)
+  const [countdown, setCountdown] = useState(COUNTDOWN)
+
+  // Set and run the countdown conditionally
+  useEffect(() => {
+    if (cancelled || opened) return
+    const interval = setInterval(() => {
+      setCountdown((prev) => (prev > 0 ? prev - 1 : 0))
+    }, 1000)
+    return () => clearInterval(interval)
+  }, [cancelled, opened])
+
+  // Navigate to sign-in page when countdown hits 0
+  useEffect(() => {
+    if (countdown > 0) return
+    navigate({ to: '/clerk/sign-in' })
+  }, [countdown, navigate])
+
+  return (
+    <div className='h-svh'>
+      <div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
+        <h1 className='text-[7rem] leading-tight font-bold'>401</h1>
+        <span className='font-medium'>Unauthorized Access</span>
+        <p className='text-muted-foreground text-center'>
+          You must be authenticated via Clerk{' '}
+          <sup>
+            <LearnMore open={opened} onOpenChange={setOpened}>
+              <p>
+                This is the same as{' '}
+                <Link
+                  to='/users'
+                  className='text-blue-500 underline decoration-dashed underline-offset-2'
+                >
+                  '/users'
+                </Link>
+                .{' '}
+              </p>
+              <p>You must first sign in using Clerk to access this route. </p>
+
+              <p className='mt-4'>
+                After signing in, you'll be able to sign out or delete your
+                account via the User Profile dropdown on this page.
+              </p>
+            </LearnMore>
+          </sup>
+          <br />
+          to access this resource.
+        </p>
+        <div className='mt-6 flex gap-4'>
+          <Button variant='outline' onClick={() => history.go(-1)}>
+            Go Back
+          </Button>
+          <Button onClick={() => navigate({ to: '/clerk/sign-in' })}>
+            <ClerkLogo className='invert' /> Sign in
+          </Button>
+        </div>
+        <div className='mt-4 h-8 text-center'>
+          {!cancelled && !opened && (
+            <>
+              <p>
+                {countdown > 0
+                  ? `Redirecting to Sign In page in ${countdown}s`
+                  : `Redirecting...`}
+              </p>
+              <Button variant='link' onClick={() => setCancelled(true)}>
+                Cancel Redirect
+              </Button>
+            </>
+          )}
+        </div>
+      </div>
+    </div>
+  )
+}

+ 130 - 0
src/routes/clerk/route.tsx

@@ -0,0 +1,130 @@
+import { createFileRoute, Outlet } from '@tanstack/react-router'
+import { IconExternalLink, IconKeyOff } from '@tabler/icons-react'
+import { ClerkProvider } from '@clerk/clerk-react'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { Separator } from '@/components/ui/separator'
+import { SidebarTrigger } from '@/components/ui/sidebar'
+import { AuthenticatedLayout } from '@/components/layout/authenticated-layout'
+import { Main } from '@/components/layout/main'
+import { ThemeSwitch } from '@/components/theme-switch'
+
+export const Route = createFileRoute('/clerk')({
+  component: RouteComponent,
+})
+
+// Import your Publishable Key
+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
+
+function RouteComponent() {
+  if (!PUBLISHABLE_KEY) {
+    return <MissingClerkPubKey />
+  }
+
+  return (
+    <ClerkProvider
+      publishableKey={PUBLISHABLE_KEY}
+      afterSignOutUrl='/clerk/sign-in'
+      signInUrl='/clerk/sign-in'
+      signUpUrl='/clerk/sign-up'
+      signInFallbackRedirectUrl='/clerk/user-management'
+      signUpFallbackRedirectUrl='/clerk/user-management'
+    >
+      <Outlet />
+    </ClerkProvider>
+  )
+}
+
+function MissingClerkPubKey() {
+  const codeBlock =
+    'bg-foreground/10 rounded-sm py-0.5 px-1 text-xs text-foreground font-bold'
+  return (
+    <AuthenticatedLayout>
+      <div className='bg-background flex h-16 justify-between p-4'>
+        <SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
+        <ThemeSwitch />
+      </div>
+      <Main className='flex flex-col items-center justify-start'>
+        <div className='max-w-2xl'>
+          <Alert>
+            <IconKeyOff className='size-4' />
+            <AlertTitle>No Publishable Key Found!</AlertTitle>
+            <AlertDescription>
+              <p className='text-balance'>
+                You need to generate a publishable key from Clerk and put it
+                inside the <code className={codeBlock}>.env</code> file.
+              </p>
+            </AlertDescription>
+          </Alert>
+
+          <h1 className='mt-4 text-2xl font-bold'>Set your Clerk API key</h1>
+          <div className='text-foreground/75 mt-4 flex flex-col gap-y-4'>
+            <ol className='list-inside list-decimal space-y-1.5'>
+              <li>
+                In the{' '}
+                <a
+                  href='https://go.clerk.com/GttUAaK'
+                  target='_blank'
+                  className='underline decoration-dashed underline-offset-4 hover:decoration-solid'
+                >
+                  Clerk
+                  <sup>
+                    <IconExternalLink className='inline-block size-4' />
+                  </sup>
+                </a>{' '}
+                Dashboard, navigate to the API keys page.
+              </li>
+              <li>
+                In the <strong>Quick Copy</strong> section, copy your Clerk
+                Publishable Key.
+              </li>
+              <li>
+                Rename <code className={codeBlock}>.env.example</code> to{' '}
+                <code className={codeBlock}>.env</code>
+              </li>
+              <li>
+                Paste your key into your <code className={codeBlock}>.env</code>{' '}
+                file.
+              </li>
+            </ol>
+            <p>The final result should resemble the following:</p>
+
+            <div className='@container space-y-2 rounded-md bg-slate-800 px-3 py-3 text-sm text-slate-200'>
+              <span className='pl-1'>.env</span>
+              <pre className='overflow-auto overscroll-x-contain rounded bg-slate-950 px-2 py-1 text-xs'>
+                <code>
+                  <span className='before:text-slate-400 md:before:pr-2 md:before:content-["1."]'>
+                    VITE_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY
+                  </span>
+                </code>
+              </pre>
+            </div>
+          </div>
+
+          <Separator className='my-4 w-full' />
+
+          <Alert>
+            <AlertTitle>Clerk Integration is Optional</AlertTitle>
+            <AlertDescription>
+              <p className='text-balance'>
+                The Clerk integration lives entirely inside{' '}
+                <code className={codeBlock}>src/routes/clerk</code>. If you plan
+                to use Clerk as your auth service, you might want to place{' '}
+                <code className={codeBlock}>ClerkProvider</code> at the root
+                route.
+              </p>
+              <p>
+                However, if you don't plan to use Clerk, you can safely remove
+                this directory and related dependency_{' '}
+                <code className={codeBlock}>@clerk/clerk-react</code>.
+              </p>
+              <p className='mt-2 text-sm'>
+                This setup is modular by design and won't affect the rest of the
+                application.
+              </p>
+            </AlertDescription>
+          </Alert>
+        </div>
+      </Main>
+    </AuthenticatedLayout>
+  )
+}