svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Avatar,
+ AvatarImage,
+ AvatarFallback,
+ AvatarBadge,
+ AvatarGroup,
+ AvatarGroupCount,
+}
diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..86e41ff
--- /dev/null
+++ b/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from 'react'
+import { ChevronRight, MoreHorizontal } from 'lucide-react'
+import { Slot } from 'radix-ui'
+
+import { cn } from '@/lib/utils'
+
+function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot.Root : 'a'
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+
svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..7fe6844
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,71 @@
+import * as React from 'react'
+import { cva } from 'class-variance-authority'
+import { Slot } from 'radix-ui'
+import { Spinner } from './spinner'
+import type { VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer data-disabled:cursor-not-allowed",
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline:
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost:
+ 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9',
+ 'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
+ 'icon-sm': 'size-8',
+ 'icon-lg': 'size-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+)
+
+function Button({
+ className,
+ variant = 'default',
+ size = 'default',
+ asChild = false,
+ isLoading = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps
& {
+ asChild?: boolean
+ isLoading?: boolean
+ }) {
+ const Comp = asChild ? Slot.Root : 'button'
+
+ return (
+ : props.children
+ }
+ />
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..db7dd3c
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+function Card({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..d8ee1ad
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,255 @@
+import * as React from 'react'
+import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
+import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui'
+
+import { cn } from '@/lib/utils'
+
+function DropdownMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function DropdownMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = 'default',
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: 'default' | 'destructive'
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+ )
+}
+
+function DropdownMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx
new file mode 100644
index 0000000..3cb9ac0
--- /dev/null
+++ b/src/components/ui/field.tsx
@@ -0,0 +1,247 @@
+import { useMemo } from 'react'
+import { cva } from 'class-variance-authority'
+import type { VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+import { Label } from '@/components/ui/label'
+import { Separator } from '@/components/ui/separator'
+
+function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
+ return (
+ [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldLegend({
+ className,
+ variant = 'legend',
+ ...props
+}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
+ return (
+
+ )
+}
+
+function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+ [data-slot=field-group]]:gap-4',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+const fieldVariants = cva(
+ 'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
+ {
+ variants: {
+ orientation: {
+ vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
+ horizontal: [
+ 'flex-row items-center',
+ '[&>[data-slot=field-label]]:flex-auto',
+ 'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
+ ],
+ responsive: [
+ 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
+ '@md/field-group:[&>[data-slot=field-label]]:flex-auto',
+ '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
+ ],
+ },
+ },
+ defaultVariants: {
+ orientation: 'vertical',
+ },
+ },
+)
+
+function Field({
+ className,
+ orientation = 'vertical',
+ ...props
+}: React.ComponentProps<'div'> & VariantProps
) {
+ return (
+
+ )
+}
+
+function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function FieldLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+ [data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
+ 'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
+ return (
+ a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'div'> & {
+ children?: React.ReactNode
+}) {
+ return (
+
+
+ {children && (
+
+ {children}
+
+ )}
+
+ )
+}
+
+function FieldError({
+ className,
+ children,
+ errors,
+ ...props
+}: React.ComponentProps<'div'> & {
+ errors?: Array<{ message?: string } | undefined>
+}) {
+ const content = useMemo(() => {
+ if (children) {
+ return children
+ }
+
+ if (!errors?.length) {
+ return null
+ }
+
+ const uniqueErrors = [
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
+ ]
+
+ if (uniqueErrors.length === 1) {
+ return uniqueErrors[0]?.message
+ }
+
+ return (
+
+ {uniqueErrors.map(
+ (error, index) =>
+ error?.message && {error.message} ,
+ )}
+
+ )
+ }, [children, errors])
+
+ if (!content) {
+ return null
+ }
+
+ return (
+
+ {content}
+
+ )
+}
+
+export {
+ Field,
+ FieldLabel,
+ FieldDescription,
+ FieldError,
+ FieldGroup,
+ FieldLegend,
+ FieldSeparator,
+ FieldSet,
+ FieldContent,
+ FieldTitle,
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..f199a06
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..1b6db8a
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react'
+import { Label as LabelPrimitive } from 'radix-ui'
+
+import { cn } from '@/lib/utils'
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000..976339f
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react'
+import { Separator as SeparatorPrimitive } from 'radix-ui'
+
+import { cn } from '@/lib/utils'
+
+function Separator({
+ className,
+ orientation = 'horizontal',
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..45f0603
--- /dev/null
+++ b/src/components/ui/sheet.tsx
@@ -0,0 +1,143 @@
+'use client'
+
+import * as React from 'react'
+import { XIcon } from 'lucide-react'
+import { Dialog as SheetPrimitive } from 'radix-ui'
+
+import { cn } from '@/lib/utils'
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return
+}
+
+function SheetTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetContent({
+ className,
+ children,
+ side = 'right',
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ side?: 'top' | 'right' | 'bottom' | 'left'
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function SheetTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
new file mode 100644
index 0000000..9f59782
--- /dev/null
+++ b/src/components/ui/sidebar.tsx
@@ -0,0 +1,729 @@
+'use client'
+
+import * as React from 'react'
+import { cva } from 'class-variance-authority'
+import { PanelLeftIcon } from 'lucide-react'
+import { Slot } from 'radix-ui'
+import type { VariantProps } from 'class-variance-authority'
+
+import { useIsMobile } from '@/hooks/useIsMobile'
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Separator } from '@/components/ui/separator'
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from '@/components/ui/sheet'
+import { Skeleton } from '@/components/ui/skeleton'
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip'
+
+const SIDEBAR_COOKIE_NAME = 'sidebar_state'
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = '16rem'
+const SIDEBAR_WIDTH_MOBILE = '18rem'
+const SIDEBAR_WIDTH_ICON = '3rem'
+const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
+
+type SidebarContextProps = {
+ state: 'expanded' | 'collapsed'
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error('useSidebar must be used within a SidebarProvider.')
+ }
+
+ return context
+}
+
+function SidebarProvider({
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<'div'> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}) {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === 'function' ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open],
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile
+ ? setOpenMobile((isOpen) => !isOpen)
+ : setOpen((isOpen) => !isOpen)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? 'expanded' : 'collapsed'
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function Sidebar({
+ side = 'left',
+ variant = 'sidebar',
+ collapsible = 'offcanvas',
+ className,
+ children,
+ ...props
+}: React.ComponentProps<'div'> & {
+ side?: 'left' | 'right'
+ variant?: 'sidebar' | 'floating' | 'inset'
+ collapsible?: 'offcanvas' | 'icon' | 'none'
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === 'none') {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+}
+
+function SidebarTrigger({
+ className,
+ onClick,
+ ...props
+}: React.ComponentProps) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+ {
+ onClick?.(event)
+ toggleSidebar()
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ )
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
+ return (
+
+ )
+}
+
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'div'> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot.Root : 'div'
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0',
+ 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot.Root : 'button'
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0',
+ // Increases the hit area of the button on mobile.
+ 'after:absolute after:-inset-2 md:after:hidden',
+ 'group-data-[collapsible=icon]:hidden',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
+ return (
+
+ )
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ )
+}
+
+const sidebarMenuButtonVariants = cva(
+ 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 cursor-pointer data-disabled:cursor-not-allowed',
+ {
+ variants: {
+ variant: {
+ default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
+ outline:
+ 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
+ },
+ size: {
+ default: 'h-8 text-sm',
+ sm: 'h-7 text-xs',
+ lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+)
+
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = 'default',
+ size = 'default',
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<'button'> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+} & VariantProps) {
+ const Comp = asChild ? Slot.Root : 'button'
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === 'string') {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+}
+
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<'button'> & {
+ asChild?: boolean
+ showOnHover?: boolean
+}) {
+ const Comp = asChild ? Slot.Root : 'button'
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0',
+ // Increases the hit area of the button on mobile.
+ 'after:absolute after:-inset-2 md:after:hidden',
+ 'peer-data-[size=sm]/menu-button:top-1',
+ 'peer-data-[size=default]/menu-button:top-1.5',
+ 'peer-data-[size=lg]/menu-button:top-2.5',
+ 'group-data-[collapsible=icon]:hidden',
+ showOnHover &&
+ 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<'div'> & {
+ showIcon?: boolean
+}) {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubButton({
+ asChild = false,
+ size = 'md',
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean
+ size?: 'sm' | 'md'
+ isActive?: boolean
+}) {
+ const Comp = asChild ? Slot.Root : 'a'
+
+ return (
+ svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
+ 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
+ size === 'sm' && 'text-xs',
+ size === 'md' && 'text-sm',
+ 'group-data-[collapsible=icon]:hidden',
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..e3beb90
--- /dev/null
+++ b/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from '@/lib/utils'
+
+function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx
new file mode 100644
index 0000000..e51fc6d
--- /dev/null
+++ b/src/components/ui/spinner.tsx
@@ -0,0 +1,16 @@
+import { Loader2Icon } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+ )
+}
+
+export { Spinner }
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..1c67b32
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react'
+import { Tooltip as TooltipPrimitive } from 'radix-ui'
+
+import { cn } from '@/lib/utils'
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/hooks/auth.helpers.ts b/src/hooks/auth.helpers.ts
new file mode 100644
index 0000000..4d7c798
--- /dev/null
+++ b/src/hooks/auth.helpers.ts
@@ -0,0 +1,38 @@
+import type { UseNavigateResult } from '@tanstack/router-core'
+import type { ClerkSession, SetActiveFunction } from '@/types/clerk'
+import { RoutesPath } from '@/types/routes'
+
+/**
+ * Creates and activates a new session, then redirects to home
+ */
+export const createSession = async (
+ result: { createdSessionId?: string | null },
+ setActive: SetActiveFunction,
+ navigate: UseNavigateResult,
+) => {
+ await setActive({
+ session: result.createdSessionId as ClerkSession | null,
+ beforeEmit: () => {
+ navigate({ to: RoutesPath.HOME.toString() })
+ },
+ })
+}
+
+/**
+ * Activates a session directly with session ID and redirects to home
+ */
+export const activateSession = async (
+ sessionId: string | null | undefined,
+ setActive: SetActiveFunction | undefined,
+ navigate: UseNavigateResult,
+) => {
+ if (!setActive) throw new Error('setActive is not available')
+ if (!sessionId) throw new Error('No session ID returned from verification')
+
+ await setActive({
+ session: sessionId,
+ beforeEmit: () => {
+ navigate({ to: RoutesPath.HOME.toString() })
+ },
+ })
+}
diff --git a/src/hooks/useIsMobile.ts b/src/hooks/useIsMobile.ts
new file mode 100644
index 0000000..4331d5c
--- /dev/null
+++ b/src/hooks/useIsMobile.ts
@@ -0,0 +1,19 @@
+import * as React from 'react'
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener('change', onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener('change', onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/src/hooks/useSignIn.ts b/src/hooks/useSignIn.ts
new file mode 100644
index 0000000..d048900
--- /dev/null
+++ b/src/hooks/useSignIn.ts
@@ -0,0 +1,114 @@
+import { useSignIn as useClerkSignIn } from '@clerk/clerk-react'
+import { useState } from 'react'
+import { useNavigate } from '@tanstack/react-router'
+import { createSession } from './auth.helpers'
+import type { SetActiveFunction } from '@/types/clerk'
+import { EMAIL_CODE_STRATEGY } from '@/types/auth'
+
+export const useSignIn = () => {
+ const router = useNavigate()
+ const { isLoaded, signIn, setActive } = useClerkSignIn()
+
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [code, setCode] = useState('')
+ const [isLoading, setIsLoading] = useState(false)
+ const [requiresMfa, setRequiresMfa] = useState(false)
+ const [error, setError] = useState(null)
+ /**
+ * Sign-in flow:
+ * 1. Authenticate with email/password
+ * 2. If MFA required, prepare second factor (email code)
+ * 3. Verify MFA code
+ * 4. Create session and redirect to home
+ */
+
+ const authenticateWithPassword = async () => {
+ const result = await signIn?.create({
+ strategy: 'password',
+ identifier: email.trim(),
+ password,
+ })
+
+ if (result?.status === 'complete') {
+ await createSession(result, setActive as SetActiveFunction, router)
+ } else if (result?.status === 'needs_second_factor') {
+ await prepareSecondFactor(result)
+ setRequiresMfa(true)
+ } else {
+ throw new Error(`Unexpected sign-in result: ${result?.status}`)
+ }
+ }
+
+ const prepareSecondFactor = async (result: any) => {
+ const hasEmailCode = result.supportedSecondFactors?.some(
+ (factor: any) => factor.strategy === EMAIL_CODE_STRATEGY,
+ )
+
+ if (hasEmailCode) {
+ await signIn?.prepareSecondFactor({
+ strategy: EMAIL_CODE_STRATEGY,
+ })
+ }
+ }
+
+ const verifyMfaCode = async () => {
+ const result = await signIn?.attemptSecondFactor({
+ strategy: EMAIL_CODE_STRATEGY,
+ code: code.trim(),
+ })
+
+ if (result?.status === 'complete') {
+ await createSession(result, setActive as SetActiveFunction, router)
+ } else {
+ throw new Error(`MFA verification failed: ${result?.status}`)
+ }
+ }
+
+ const handleSignIn = async (e: React.SubmitEvent) => {
+ e.preventDefault()
+
+ if (!isLoaded || !email.trim()) return
+
+ setError(null)
+ setIsLoading(true)
+ try {
+ await authenticateWithPassword()
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Sign in failed'
+ setError(message)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleMfaVerify = async (e: React.SubmitEvent) => {
+ e.preventDefault()
+
+ setError(null)
+ setIsLoading(true)
+ try {
+ await verifyMfaCode()
+ } catch (err) {
+ const message =
+ err instanceof Error ? err.message : 'MFA verification failed'
+ setError(message)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return {
+ handleSignIn,
+ handleMfaVerify,
+ email,
+ setEmail,
+ password,
+ setPassword,
+ code,
+ setCode,
+ isLoading,
+ requiresMfa,
+ error,
+ }
+}
diff --git a/src/hooks/useSignOut.ts b/src/hooks/useSignOut.ts
new file mode 100644
index 0000000..c01707c
--- /dev/null
+++ b/src/hooks/useSignOut.ts
@@ -0,0 +1,13 @@
+import { useClerk } from '@clerk/clerk-react'
+
+export const useSignOut = () => {
+ const { signOut } = useClerk()
+
+ const handleSignOut = async () => {
+ await signOut()
+ }
+
+ return {
+ handleSignOut,
+ }
+}
diff --git a/src/hooks/useSignUp.ts b/src/hooks/useSignUp.ts
new file mode 100644
index 0000000..947ff9b
--- /dev/null
+++ b/src/hooks/useSignUp.ts
@@ -0,0 +1,107 @@
+import { useSignUp as useClerkSignUp } from '@clerk/clerk-react'
+import { useNavigate } from '@tanstack/react-router'
+import { useState } from 'react'
+import { activateSession } from './auth.helpers'
+import type { SetActiveFunction } from '@/types/clerk'
+import { EMAIL_CODE_STRATEGY } from '@/types/auth'
+
+export const useSignUp = () => {
+ const router = useNavigate()
+ const { isLoaded, signUp, setActive } = useClerkSignUp()
+
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [verificationCode, setVerificationCode] = useState('')
+ const [pendingVerification, setPendingVerification] = useState(false)
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ /**
+ * Sign-up flow:
+ * 1. Create account with email/password
+ * 2. Request email verification code
+ * 3. Verify email code
+ * 4. Activate session and redirect to home
+ */
+
+ const createAccount = async () => {
+ const result = await signUp?.create({
+ emailAddress: email,
+ password,
+ })
+
+ if (result?.status === 'missing_requirements') {
+ await signUp?.prepareEmailAddressVerification({
+ strategy: EMAIL_CODE_STRATEGY,
+ })
+ setPendingVerification(true)
+ } else {
+ throw new Error(`Unexpected signup result: ${result?.status}`)
+ }
+ }
+
+ const verifyEmailCode = async () => {
+ const result = await signUp?.attemptEmailAddressVerification({
+ code: verificationCode,
+ })
+
+ if (result?.status === 'complete') {
+ await activateSession(
+ result.createdSessionId,
+ setActive as SetActiveFunction,
+ router,
+ )
+ } else {
+ throw new Error(`Verification failed: ${result?.status}`)
+ }
+ }
+
+ const handleSignUp = async (e: React.SubmitEvent) => {
+ e.preventDefault()
+
+ if (!isLoaded) return
+
+ setError(null)
+ setIsLoading(true)
+ try {
+ await createAccount()
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Sign up failed'
+ setError(message)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleVerification = async (e: React.SubmitEvent) => {
+ e.preventDefault()
+
+ setError(null)
+ setIsLoading(true)
+ try {
+ await verifyEmailCode()
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Verification failed'
+ setError(message)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return {
+ handleSignUp,
+ handleVerification,
+ email,
+ setEmail,
+ password,
+ setPassword,
+ confirmPassword,
+ setConfirmPassword,
+ verificationCode,
+ setVerificationCode,
+ isLoading,
+ pendingVerification,
+ error,
+ }
+}
diff --git a/src/integrations/clerk/header-user.tsx b/src/integrations/clerk/header-user.tsx
index 69b4e83..fd99c5c 100644
--- a/src/integrations/clerk/header-user.tsx
+++ b/src/integrations/clerk/header-user.tsx
@@ -1,6 +1,6 @@
import {
- SignedIn,
SignInButton,
+ SignedIn,
SignedOut,
UserButton,
} from '@clerk/clerk-react'
diff --git a/src/lib/demo-store-devtools.tsx b/src/lib/demo-store-devtools.tsx
index 9b66f01..3a7f2d6 100644
--- a/src/lib/demo-store-devtools.tsx
+++ b/src/lib/demo-store-devtools.tsx
@@ -1,7 +1,7 @@
import { EventClient } from '@tanstack/devtools-event-client'
-import { useState, useEffect } from 'react'
+import { useEffect, useState } from 'react'
-import { store, fullName } from './demo-store'
+import { fullName, store } from './demo-store'
type EventMap = {
'store-devtools:state': {
@@ -45,15 +45,15 @@ function DevtoolPanel() {
First Name
- {state?.firstName}
+ {state.firstName}
Last Name
- {state?.lastName}
+ {state.lastName}
Full Name
- {state?.fullName}
+ {state.fullName}
)
}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index fed2fe9..914ec86 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,7 @@
-import { clsx, type ClassValue } from 'clsx'
+import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
+import type { ClassValue } from 'clsx'
-export function cn(...inputs: ClassValue[]) {
+export function cn(...inputs: Array) {
return twMerge(clsx(inputs))
}
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index dceedff..718da20 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -10,33 +10,53 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AuthSignupRouteImport } from './routes/auth/signup'
+import { Route as AuthSigninRouteImport } from './routes/auth/signin'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const AuthSignupRoute = AuthSignupRouteImport.update({
+ id: '/auth/signup',
+ path: '/auth/signup',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const AuthSigninRoute = AuthSigninRouteImport.update({
+ id: '/auth/signin',
+ path: '/auth/signin',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
+ '/auth/signin': typeof AuthSigninRoute
+ '/auth/signup': typeof AuthSignupRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
+ '/auth/signin': typeof AuthSigninRoute
+ '/auth/signup': typeof AuthSignupRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
+ '/auth/signin': typeof AuthSigninRoute
+ '/auth/signup': typeof AuthSignupRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/'
+ fullPaths: '/' | '/auth/signin' | '/auth/signup'
fileRoutesByTo: FileRoutesByTo
- to: '/'
- id: '__root__' | '/'
+ to: '/' | '/auth/signin' | '/auth/signup'
+ id: '__root__' | '/' | '/auth/signin' | '/auth/signup'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
+ AuthSigninRoute: typeof AuthSigninRoute
+ AuthSignupRoute: typeof AuthSignupRoute
}
declare module '@tanstack/react-router' {
@@ -48,11 +68,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/auth/signup': {
+ id: '/auth/signup'
+ path: '/auth/signup'
+ fullPath: '/auth/signup'
+ preLoaderRoute: typeof AuthSignupRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/auth/signin': {
+ id: '/auth/signin'
+ path: '/auth/signin'
+ fullPath: '/auth/signin'
+ preLoaderRoute: typeof AuthSigninRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
+ AuthSigninRoute: AuthSigninRoute,
+ AuthSignupRoute: AuthSignupRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index d1e3a03..d2fc906 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -6,7 +6,6 @@ import {
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
-import Header from '../components/Header'
import NotFound from '../components/NotFound'
import ClerkProvider from '../integrations/clerk/provider'
@@ -71,7 +70,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
-
{children}
+
+
+
+
+ )
+}
diff --git a/src/routes/auth/signup.tsx b/src/routes/auth/signup.tsx
new file mode 100644
index 0000000..105862e
--- /dev/null
+++ b/src/routes/auth/signup.tsx
@@ -0,0 +1,17 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { SignUpForm } from '@/components/Forms/SignUp'
+import { RoutesPath } from '@/types/routes'
+
+export const Route = createFileRoute(RoutesPath.SIGN_UP)({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+