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
This commit is contained in:
Sat Naing
2025-05-25 11:44:24 +07:00
committed by GitHub
parent f8fa601b3f
commit be3d7d884d
19 changed files with 868 additions and 36 deletions

1
.env.example Normal file
View File

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

2
.gitignore vendored
View File

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

View File

@@ -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. Dont 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)

View File

@@ -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
pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -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
src/assets/clerk-logo.tsx Normal file
View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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',
},
],
},
],
},
{

View File

@@ -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}
/>

View File

@@ -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>
)
}

View File

@@ -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"

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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]' />}
/>
),
})

View File

@@ -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]' />} />
),
})

View File

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

View File

@@ -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
src/routes/clerk/route.tsx Normal file
View File

@@ -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>
)
}