[rewrite] Updated frontend structure and played with custom routes

This commit is contained in:
Thastertyn committed 2025-02-21 14:51:17 +01:00
1 parent 43063dcf4e
commit 073f01d070
243 files changed
+8744 -8906

No files matched your search

+5 -1
View File
@@ -1,7 +1,11 @@
{
// Backend settings
"files.exclude": {
"**/__pycache__/**": true
},
"mypy-type-checker.args": ["--config-file='backend/mypy.ini'"],
"python.defaultInterpreterPath": "./backend/.venv/bin/python"
"python.defaultInterpreterPath": "./backend/.venv/bin/python",
// Frontend settings
"prettier.configPath": "./frontend/.prettierrc"
}
+10
View File
@@ -0,0 +1,10 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"proseWrap": "always",
"requireConfig": false,
"useTabs": true,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"bracketSameLine": true
}
-42
View File
@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
-16
View File
@@ -1,16 +0,0 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
export default function App() {
return (
<div className="min-h-screen bg-gray-100 p-6 flex flex-col items-center">
<Card className="max-w-md text-center p-4">
<CardContent>
<h1 className="text-2xl font-bold">Welcome to My ShadCN Site</h1>
<p className="mt-2">This is a sample site using Vite, TypeScript, and ShadCN UI.</p>
<Button className="mt-4">Click Me</Button>
</CardContent>
</Card>
</div>
);
}
+45 -45
View File
@@ -1,6 +1,6 @@
"use client"
"use client";
import * as React from "react"
import * as React from "react";
import {
AudioWaveform,
BookOpen,
@@ -11,44 +11,44 @@ import {
Map,
PieChart,
Settings2,
SquareTerminal,
} from "lucide-react"
SquareTerminal
} from "lucide-react";
import { NavMain } from "@/components/nav-main"
import { NavProjects } from "@/components/nav-projects"
import { NavUser } from "@/components/nav-user"
import { TeamSwitcher } from "@/components/team-switcher"
import { NavMain } from "@/components/nav-main";
import { NavProjects } from "@/components/nav-projects";
import { NavUser } from "@/components/nav-user";
import { TeamSwitcher } from "@/components/team-switcher";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from "@/components/ui/sidebar"
SidebarRail
} from "@/components/ui/sidebar";
// This is sample data.
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
avatar: "/avatars/shadcn.jpg"
},
teams: [
{
name: "Acme Inc",
logo: GalleryVerticalEnd,
plan: "Enterprise",
plan: "Enterprise"
},
{
name: "Acme Corp.",
logo: AudioWaveform,
plan: "Startup",
plan: "Startup"
},
{
name: "Evil Corp.",
logo: Command,
plan: "Free",
},
plan: "Free"
}
],
navMain: [
{
@@ -59,17 +59,17 @@ const data = {
items: [
{
title: "History",
url: "#",
url: "#"
},
{
title: "Starred",
url: "#",
url: "#"
},
{
title: "Settings",
url: "#",
},
],
url: "#"
}
]
},
{
title: "Models",
@@ -78,17 +78,17 @@ const data = {
items: [
{
title: "Genesis",
url: "#",
url: "#"
},
{
title: "Explorer",
url: "#",
url: "#"
},
{
title: "Quantum",
url: "#",
},
],
url: "#"
}
]
},
{
title: "Documentation",
@@ -97,21 +97,21 @@ const data = {
items: [
{
title: "Introduction",
url: "#",
url: "#"
},
{
title: "Get Started",
url: "#",
url: "#"
},
{
title: "Tutorials",
url: "#",
url: "#"
},
{
title: "Changelog",
url: "#",
},
],
url: "#"
}
]
},
{
title: "Settings",
@@ -120,41 +120,41 @@ const data = {
items: [
{
title: "General",
url: "#",
url: "#"
},
{
title: "Team",
url: "#",
url: "#"
},
{
title: "Billing",
url: "#",
url: "#"
},
{
title: "Limits",
url: "#",
},
],
},
url: "#"
}
]
}
],
projects: [
{
name: "Design Engineering",
url: "#",
icon: Frame,
icon: Frame
},
{
name: "Sales & Marketing",
url: "#",
icon: PieChart,
icon: PieChart
},
{
name: "Travel",
url: "#",
icon: Map,
},
],
}
icon: Map
}
]
};
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
@@ -171,5 +171,5 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
);
}
+6 -6
View File
@@ -1,16 +1,16 @@
import { IconPlanet } from '@tabler/icons-react'
import { IconPlanet } from "@tabler/icons-react";
export default function ComingSoon() {
return (
<div className='h-svh'>
<div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
<div className="h-svh">
<div className="m-auto flex h-full w-full flex-col items-center justify-center gap-2">
<IconPlanet size={72} />
<h1 className='text-4xl font-bold leading-tight'>Coming Soon 👀</h1>
<p className='text-center text-muted-foreground'>
<h1 className="text-4xl font-bold leading-tight">Coming Soon 👀</h1>
<p className="text-center text-muted-foreground">
This page has not been created yet. <br />
Stay tuned though!
</p>
</div>
</div>
)
);
}
+34 -36
View File
@@ -1,13 +1,13 @@
import React from 'react'
import { useNavigate } from '@tanstack/react-router'
import React from "react";
import { useNavigate } from "@tanstack/react-router";
import {
IconArrowRightDashed,
IconDeviceLaptop,
IconMoon,
IconSun,
} from '@tabler/icons-react'
import { useSearch } from '@/context/search-context'
import { useTheme } from '@/context/theme-context'
IconSun
} from "@tabler/icons-react";
import { useSearch } from "@/context/search-context";
import { useTheme } from "@/context/theme-context";
import {
CommandDialog,
CommandEmpty,
@@ -15,29 +15,29 @@ import {
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command'
import { sidebarData } from './layout/data/sidebar-data'
import { ScrollArea } from './ui/scroll-area'
CommandSeparator
} from "@/components/ui/command";
import { sidebarData } from "./layout/data/sidebar-data";
import { ScrollArea } from "./ui/scroll-area";
export function CommandMenu() {
const navigate = useNavigate()
const { setTheme } = useTheme()
const { open, setOpen } = useSearch()
const navigate = useNavigate();
const { setTheme } = useTheme();
const { open, setOpen } = useSearch();
const runCommand = React.useCallback(
(command: () => unknown) => {
setOpen(false)
command()
setOpen(false);
command();
},
[setOpen]
)
);
return (
<CommandDialog modal open={open} onOpenChange={setOpen}>
<CommandInput placeholder='Type a command or search...' />
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<ScrollArea type='hover' className='h-72 pr-1'>
<ScrollArea type="hover" className="h-72 pr-1">
<CommandEmpty>No results found.</CommandEmpty>
{sidebarData.navGroups.map((group) => (
<CommandGroup key={group.title} heading={group.title}>
@@ -48,43 +48,41 @@ export function CommandMenu() {
key={`${navItem.url}-${i}`}
value={navItem.title}
onSelect={() => {
runCommand(() => navigate({ to: navItem.url }))
}}
>
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
<IconArrowRightDashed className='size-2 text-muted-foreground/80' />
runCommand(() => navigate({ to: navItem.url }));
}}>
<div className="mr-2 flex h-4 w-4 items-center justify-center">
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
</div>
{navItem.title}
</CommandItem>
)
);
return navItem.items?.map((subItem, i) => (
<CommandItem
key={`${subItem.url}-${i}`}
value={subItem.title}
onSelect={() => {
runCommand(() => navigate({ to: subItem.url }))
}}
>
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
<IconArrowRightDashed className='size-2 text-muted-foreground/80' />
runCommand(() => navigate({ to: subItem.url }));
}}>
<div className="mr-2 flex h-4 w-4 items-center justify-center">
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
</div>
{subItem.title}
</CommandItem>
))
));
})}
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup heading='Theme'>
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
<CommandGroup heading="Theme">
<CommandItem onSelect={() => runCommand(() => setTheme("light"))}>
<IconSun /> <span>Light</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
<IconMoon className='scale-90' />
<CommandItem onSelect={() => runCommand(() => setTheme("dark"))}>
<IconMoon className="scale-90" />
<span>Dark</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme('system'))}>
<CommandItem onSelect={() => runCommand(() => setTheme("system"))}>
<IconDeviceLaptop />
<span>System</span>
</CommandItem>
@@ -92,5 +90,5 @@ export function CommandMenu() {
</ScrollArea>
</CommandList>
</CommandDialog>
)
);
}
+23 -24
View File
@@ -1,4 +1,4 @@
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
import {
AlertDialog,
AlertDialogCancel,
@@ -6,23 +6,23 @@ import {
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
AlertDialogTitle
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
interface ConfirmDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
title: React.ReactNode
disabled?: boolean
desc: React.JSX.Element | string
cancelBtnText?: string
confirmText?: React.ReactNode
destructive?: boolean
handleConfirm: () => void
isLoading?: boolean
className?: string
children?: React.ReactNode
open: boolean;
onOpenChange: (open: boolean) => void;
title: React.ReactNode;
disabled?: boolean;
desc: React.JSX.Element | string;
cancelBtnText?: string;
confirmText?: React.ReactNode;
destructive?: boolean;
handleConfirm: () => void;
isLoading?: boolean;
className?: string;
children?: React.ReactNode;
}
export function ConfirmDialog(props: ConfirmDialogProps) {
@@ -38,11 +38,11 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
disabled = false,
handleConfirm,
...actions
} = props
} = props;
return (
<AlertDialog {...actions}>
<AlertDialogContent className={cn(className && className)}>
<AlertDialogHeader className='text-left'>
<AlertDialogHeader className="text-left">
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription asChild>
<div>{desc}</div>
@@ -51,17 +51,16 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
{children}
<AlertDialogFooter>
<AlertDialogCancel disabled={isLoading}>
{cancelBtnText ?? 'Cancel'}
{cancelBtnText ?? "Cancel"}
</AlertDialogCancel>
<Button
variant={destructive ? 'destructive' : 'default'}
variant={destructive ? "destructive" : "default"}
onClick={handleConfirm}
disabled={disabled || isLoading}
>
{confirmText ?? 'Continue'}
disabled={disabled || isLoading}>
{confirmText ?? "Continue"}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
);
}
@@ -3,16 +3,16 @@ import {
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from '@/components/ui/sidebar'
import { NavGroup } from '@/components/layout/nav-group'
import { NavUser } from '@/components/layout/nav-user'
import { TeamSwitcher } from '@/components/layout/team-switcher'
import { sidebarData } from './data/sidebar-data'
SidebarRail
} from "@/components/ui/sidebar";
import { NavGroup } from "@/components/layout/nav-group";
import { NavUser } from "@/components/layout/nav-user";
import { TeamSwitcher } from "@/components/layout/team-switcher";
import { sidebarData } from "./data/sidebar-data";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible='icon' variant='floating' {...props}>
<Sidebar collapsible="icon" variant="floating" {...props}>
<SidebarHeader>
<TeamSwitcher teams={sidebarData.teams} />
</SidebarHeader>
@@ -26,5 +26,5 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
);
}
@@ -17,168 +17,173 @@ import {
IconTool,
IconUserCog,
IconUserOff,
IconUsers,
} from '@tabler/icons-react'
import { AudioWaveform, Command, GalleryVerticalEnd } from 'lucide-react'
import { type SidebarData } from '../types'
IconUsers
} from "@tabler/icons-react";
import { AudioWaveform, Command, GalleryVerticalEnd } from "lucide-react";
import { type SidebarData } from "../types";
export const sidebarData: SidebarData = {
user: {
name: 'satnaing',
email: 'satnaingdev@gmail.com',
avatar: '/avatars/shadcn.jpg',
name: "satnaing",
email: "satnaingdev@gmail.com",
avatar: "/avatars/shadcn.jpg"
},
teams: [
{
name: 'Shadcn Admin',
name: "Shadcn Admin",
logo: Command,
plan: 'Vite + ShadcnUI',
plan: "Vite + ShadcnUI"
},
{
name: 'Acme Inc',
name: "Acme Inc",
logo: GalleryVerticalEnd,
plan: 'Enterprise',
plan: "Enterprise"
},
{
name: 'Acme Corp.',
name: "Acme Corp.",
logo: AudioWaveform,
plan: 'Startup',
},
plan: "Startup"
}
],
navGroups: [
{
title: 'General',
title: "General",
items: [
{
title: 'Dashboard',
url: '/',
icon: IconLayoutDashboard,
title: "Dashboard",
url: "/",
icon: IconLayoutDashboard
},
{
title: 'Tasks',
url: '/tasks',
icon: IconChecklist,
title: "Tasks",
url: "/tasks",
icon: IconChecklist
},
{
title: 'Apps',
url: '/apps',
icon: IconPackages,
title: "Apps",
url: "/apps",
icon: IconPackages
},
{
title: 'Chats',
url: '/chats',
badge: '3',
icon: IconMessages,
title: "Chats",
url: "/chats",
badge: "3",
icon: IconMessages
},
{
title: 'Users',
url: '/users',
icon: IconUsers,
},
],
title: "Users",
url: "/users",
icon: IconUsers
},
{
title: 'Pages',
title: "NBA",
url: "/nba",
icon: IconChecklist
}
]
},
{
title: "Pages",
items: [
{
title: 'Auth',
title: "Auth",
icon: IconLockAccess,
items: [
{
title: 'Sign In',
url: '/sign-in',
title: "Sign In",
url: "/sign-in"
},
{
title: 'Sign In (2 Col)',
url: '/sign-in-2',
title: "Sign In (2 Col)",
url: "/sign-in-2"
},
{
title: 'Sign Up',
url: '/sign-up',
title: "Sign Up",
url: "/sign-up"
},
{
title: 'Forgot Password',
url: '/forgot-password',
title: "Forgot Password",
url: "/forgot-password"
},
{
title: 'OTP',
url: '/otp',
},
],
title: "OTP",
url: "/otp"
}
]
},
{
title: 'Errors',
title: "Errors",
icon: IconBug,
items: [
{
title: 'Unauthorized',
url: '/401',
icon: IconLock,
title: "Unauthorized",
url: "/401",
icon: IconLock
},
{
title: 'Forbidden',
url: '/403',
icon: IconUserOff,
title: "Forbidden",
url: "/403",
icon: IconUserOff
},
{
title: 'Not Found',
url: '/404',
icon: IconError404,
title: "Not Found",
url: "/404",
icon: IconError404
},
{
title: 'Internal Server Error',
url: '/500',
icon: IconServerOff,
title: "Internal Server Error",
url: "/500",
icon: IconServerOff
},
{
title: 'Maintenance Error',
url: '/503',
icon: IconBarrierBlock,
},
],
},
],
title: "Maintenance Error",
url: "/503",
icon: IconBarrierBlock
}
]
}
]
},
{
title: 'Other',
title: "Other",
items: [
{
title: 'Settings',
title: "Settings",
icon: IconSettings,
items: [
{
title: 'Profile',
url: '/settings',
icon: IconUserCog,
title: "Profile",
url: "/settings",
icon: IconUserCog
},
{
title: 'Account',
url: '/settings/account',
icon: IconTool,
title: "Account",
url: "/settings/account",
icon: IconTool
},
{
title: 'Appearance',
url: '/settings/appearance',
icon: IconPalette,
title: "Appearance",
url: "/settings/appearance",
icon: IconPalette
},
{
title: 'Notifications',
url: '/settings/notifications',
icon: IconNotification,
title: "Notifications",
url: "/settings/notifications",
icon: IconNotification
},
{
title: 'Display',
url: '/settings/display',
icon: IconBrowserCheck,
},
],
title: "Display",
url: "/settings/display",
icon: IconBrowserCheck
}
]
},
{
title: 'Help Center',
url: '/help-center',
icon: IconHelp,
},
],
},
],
}
title: "Help Center",
url: "/help-center",
icon: IconHelp
}
]
}
]
};
+21 -22
View File
@@ -1,11 +1,11 @@
import React from 'react'
import { cn } from '@/lib/utils'
import { Separator } from '@/components/ui/separator'
import { SidebarTrigger } from '@/components/ui/sidebar'
import React from "react";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar";
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
fixed?: boolean
ref?: React.Ref<HTMLElement>
fixed?: boolean;
ref?: React.Ref<HTMLElement>;
}
export const Header = ({
@@ -14,35 +14,34 @@ export const Header = ({
children,
...props
}: HeaderProps) => {
const [offset, setOffset] = React.useState(0)
const [offset, setOffset] = React.useState(0);
React.useEffect(() => {
const onScroll = () => {
setOffset(document.body.scrollTop || document.documentElement.scrollTop)
}
setOffset(document.body.scrollTop || document.documentElement.scrollTop);
};
// Add scroll listener to the body
document.addEventListener('scroll', onScroll, { passive: true })
document.addEventListener("scroll", onScroll, { passive: true });
// Clean up the event listener on unmount
return () => document.removeEventListener('scroll', onScroll)
}, [])
return () => document.removeEventListener("scroll", onScroll);
}, []);
return (
<header
className={cn(
'flex h-16 items-center gap-3 bg-background p-4 sm:gap-4',
fixed && 'header-fixed peer/header fixed z-50 w-[inherit] rounded-md',
offset > 10 && fixed ? 'shadow' : 'shadow-none',
"flex h-16 items-center gap-3 bg-background p-4 sm:gap-4",
fixed && "header-fixed peer/header fixed z-50 w-[inherit] rounded-md",
offset > 10 && fixed ? "shadow" : "shadow-none",
className
)}
{...props}
>
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
<Separator orientation='vertical' className='h-6' />
{...props}>
<SidebarTrigger variant="outline" className="scale-125 sm:scale-100" />
<Separator orientation="vertical" className="h-6" />
{children}
</header>
)
}
);
};
Header.displayName = 'Header'
Header.displayName = "Header";
+10 -10
View File
@@ -1,22 +1,22 @@
import React from 'react'
import { cn } from '@/lib/utils'
import React from "react";
import { cn } from "@/lib/utils";
interface MainProps extends React.HTMLAttributes<HTMLElement> {
fixed?: boolean
ref?: React.Ref<HTMLElement>
fixed?: boolean;
ref?: React.Ref<HTMLElement>;
}
export const Main = ({ fixed, ...props }: MainProps) => {
return (
<main
className={cn(
'peer-[.header-fixed]/header:mt-16',
'px-4 py-6',
fixed && 'fixed-main flex flex-grow flex-col overflow-hidden'
"peer-[.header-fixed]/header:mt-16",
"px-4 py-6",
fixed && "fixed-main flex flex-grow flex-col overflow-hidden"
)}
{...props}
/>
)
}
);
};
Main.displayName = 'Main'
Main.displayName = "Main";
+51 -56
View File
@@ -1,11 +1,11 @@
import { ReactNode } from 'react'
import { Link, useLocation } from '@tanstack/react-router'
import { ChevronRight } from 'lucide-react'
import { ReactNode } from "react";
import { Link, useLocation } from "@tanstack/react-router";
import { ChevronRight } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
CollapsibleTrigger
} from "@/components/ui/collapsible";
import {
SidebarGroup,
SidebarGroupLabel,
@@ -15,57 +15,56 @@ import {
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from '@/components/ui/sidebar'
import { Badge } from '../ui/badge'
useSidebar
} from "@/components/ui/sidebar";
import { Badge } from "../ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../ui/dropdown-menu'
import { NavCollapsible, NavItem, NavLink, type NavGroup } from './types'
DropdownMenuTrigger
} from "../ui/dropdown-menu";
import { NavCollapsible, NavItem, NavLink, type NavGroup } from "./types";
export function NavGroup({ title, items }: NavGroup) {
const { state } = useSidebar()
const href = useLocation({ select: (location) => location.href })
const { state } = useSidebar();
const href = useLocation({ select: (location) => location.href });
return (
<SidebarGroup>
<SidebarGroupLabel>{title}</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => {
const key = `${item.title}-${item.url}`
const key = `${item.title}-${item.url}`;
if (!item.items)
return <SidebarMenuLink key={key} item={item} href={href} />
return <SidebarMenuLink key={key} item={item} href={href} />;
if (state === 'collapsed')
if (state === "collapsed")
return (
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
)
);
return <SidebarMenuCollapsible key={key} item={item} href={href} />
return <SidebarMenuCollapsible key={key} item={item} href={href} />;
})}
</SidebarMenu>
</SidebarGroup>
)
);
}
const NavBadge = ({ children }: { children: ReactNode }) => (
<Badge className='rounded-full px-1 py-0 text-xs'>{children}</Badge>
)
<Badge className="rounded-full px-1 py-0 text-xs">{children}</Badge>
);
const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
const { setOpenMobile } = useSidebar()
const { setOpenMobile } = useSidebar();
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={checkIsActive(href, item)}
tooltip={item.title}
>
tooltip={item.title}>
<Link to={item.url} onClick={() => setOpenMobile(false)}>
{item.icon && <item.icon />}
<span>{item.title}</span>
@@ -73,40 +72,38 @@ const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
}
);
};
const SidebarMenuCollapsible = ({
item,
href,
href
}: {
item: NavCollapsible
href: string
item: NavCollapsible;
href: string;
}) => {
const { setOpenMobile } = useSidebar()
const { setOpenMobile } = useSidebar();
return (
<Collapsible
asChild
defaultOpen={checkIsActive(href, item, true)}
className='group/collapsible'
>
className="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent className='CollapsibleContent'>
<CollapsibleContent className="CollapsibleContent">
<SidebarMenuSub>
{item.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
isActive={checkIsActive(href, subItem)}
>
isActive={checkIsActive(href, subItem)}>
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
{subItem.icon && <subItem.icon />}
<span>{subItem.title}</span>
@@ -119,15 +116,15 @@ const SidebarMenuCollapsible = ({
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
}
);
};
const SidebarMenuCollapsedDropdown = ({
item,
href,
href
}: {
item: NavCollapsible
href: string
item: NavCollapsible;
href: string;
}) => {
return (
<SidebarMenuItem>
@@ -135,29 +132,27 @@ const SidebarMenuCollapsedDropdown = ({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={checkIsActive(href, item)}
>
isActive={checkIsActive(href, item)}>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.badge && <NavBadge>{item.badge}</NavBadge>}
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent side='right' align='start' sideOffset={4}>
<DropdownMenuContent side="right" align="start" sideOffset={4}>
<DropdownMenuLabel>
{item.title} {item.badge ? `(${item.badge})` : ''}
{item.title} {item.badge ? `(${item.badge})` : ""}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{item.items.map((sub) => (
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
<Link
to={sub.url}
className={`${checkIsActive(href, sub) ? 'bg-secondary' : ''}`}
>
className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}>
{sub.icon && <sub.icon />}
<span className='max-w-52 text-wrap'>{sub.title}</span>
<span className="max-w-52 text-wrap">{sub.title}</span>
{sub.badge && (
<span className='ml-auto text-xs'>{sub.badge}</span>
<span className="ml-auto text-xs">{sub.badge}</span>
)}
</Link>
</DropdownMenuItem>
@@ -165,16 +160,16 @@ const SidebarMenuCollapsedDropdown = ({
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
)
}
);
};
function checkIsActive(href: string, item: NavItem, mainNav = false) {
return (
href === item.url || // /endpint?search=param
href.split('?')[0] === item.url || // endpoint
href.split("?")[0] === item.url || // endpoint
!!item?.items?.filter((i) => i.url === href).length || // if child nav is active
(mainNav &&
href.split('/')[1] !== '' &&
href.split('/')[1] === item?.url?.split('/')[1])
)
href.split("/")[1] !== "" &&
href.split("/")[1] === item?.url?.split("/")[1])
);
}
+37 -39
View File
@@ -1,13 +1,13 @@
import { Link } from '@tanstack/react-router'
import { Link } from "@tanstack/react-router";
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
Sparkles
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
@@ -15,25 +15,25 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar'
useSidebar
} from "@/components/ui/sidebar";
export function NavUser({
user,
user
}: {
user: {
name: string
email: string
avatar: string
}
name: string;
email: string;
avatar: string;
};
}) {
const { isMobile } = useSidebar()
const { isMobile } = useSidebar();
return (
<SidebarMenu>
@@ -41,35 +41,33 @@ export function NavUser({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size='lg'
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
>
<Avatar className='h-8 w-8 rounded-lg'>
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
<AvatarFallback className="rounded-lg">SN</AvatarFallback>
</Avatar>
<div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-semibold'>{user.name}</span>
<span className='truncate text-xs'>{user.email}</span>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className='ml-auto size-4' />
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg'
side={isMobile ? 'bottom' : 'right'}
align='end'
sideOffset={4}
>
<DropdownMenuLabel className='p-0 font-normal'>
<div className='flex items-center gap-2 px-1 py-1.5 text-left text-sm'>
<Avatar className='h-8 w-8 rounded-lg'>
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
<AvatarFallback className="rounded-lg">SN</AvatarFallback>
</Avatar>
<div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-semibold'>{user.name}</span>
<span className='truncate text-xs'>{user.email}</span>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
@@ -83,19 +81,19 @@ export function NavUser({
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link to='/settings/account'>
<Link to="/settings/account">
<BadgeCheck />
Account
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to='/settings'>
<Link to="/settings">
<CreditCard />
Billing
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to='/settings/notifications'>
<Link to="/settings/notifications">
<Bell />
Notifications
</Link>
@@ -110,5 +108,5 @@ export function NavUser({
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
);
}
@@ -1,5 +1,5 @@
import * as React from 'react'
import { ChevronsUpDown, Plus } from 'lucide-react'
import * as React from "react";
import { ChevronsUpDown, Plus } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
@@ -7,26 +7,26 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar'
useSidebar
} from "@/components/ui/sidebar";
export function TeamSwitcher({
teams,
teams
}: {
teams: {
name: string
logo: React.ElementType
plan: string
}[]
name: string;
logo: React.ElementType;
plan: string;
}[];
}) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
const { isMobile } = useSidebar();
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
return (
<SidebarMenu>
@@ -34,53 +34,50 @@ export function TeamSwitcher({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size='lg'
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
>
<div className='flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground'>
<activeTeam.logo className='size-4' />
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
<div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-semibold'>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{activeTeam.name}
</span>
<span className='truncate text-xs'>{activeTeam.plan}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className='ml-auto' />
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg'
align='start'
side={isMobile ? 'bottom' : 'right'}
sideOffset={4}
>
<DropdownMenuLabel className='text-xs text-muted-foreground'>
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className='gap-2 p-2'
>
<div className='flex size-6 items-center justify-center rounded-sm border'>
<team.logo className='size-4 shrink-0' />
className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className='gap-2 p-2'>
<div className='flex size-6 items-center justify-center rounded-md border bg-background'>
<Plus className='size-4' />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className='font-medium text-muted-foreground'>Add team</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
);
}
+20 -23
View File
@@ -1,41 +1,40 @@
import { Link } from '@tanstack/react-router'
import { IconMenu } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Link } from "@tanstack/react-router";
import { IconMenu } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
links: {
title: string
href: string
isActive: boolean
disabled?: boolean
}[]
title: string;
href: string;
isActive: boolean;
disabled?: boolean;
}[];
}
export function TopNav({ className, links, ...props }: TopNavProps) {
return (
<>
<div className='md:hidden'>
<div className="md:hidden">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button size='icon' variant='outline'>
<Button size="icon" variant="outline">
<IconMenu />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side='bottom' align='start'>
<DropdownMenuContent side="bottom" align="start">
{links.map(({ title, href, isActive, disabled }) => (
<DropdownMenuItem key={`${title}-${href}`} asChild>
<Link
to={href}
className={!isActive ? 'text-muted-foreground' : ''}
disabled={disabled}
>
className={!isActive ? "text-muted-foreground" : ""}
disabled={disabled}>
{title}
</Link>
</DropdownMenuItem>
@@ -46,22 +45,20 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
<nav
className={cn(
'hidden items-center space-x-4 md:flex lg:space-x-6',
"hidden items-center space-x-4 md:flex lg:space-x-6",
className
)}
{...props}
>
{...props}>
{links.map(({ title, href, isActive, disabled }) => (
<Link
key={`${title}-${href}`}
to={href}
disabled={disabled}
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? '' : 'text-muted-foreground'}`}
>
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? "" : "text-muted-foreground"}`}>
{title}
</Link>
))}
</nav>
</>
)
);
}
+23 -23
View File
@@ -1,44 +1,44 @@
import { LinkProps } from '@tanstack/react-router'
import { LinkProps } from "@tanstack/react-router";
interface User {
name: string
email: string
avatar: string
name: string;
email: string;
avatar: string;
}
interface Team {
name: string
logo: React.ElementType
plan: string
name: string;
logo: React.ElementType;
plan: string;
}
interface BaseNavItem {
title: string
badge?: string
icon?: React.ElementType
title: string;
badge?: string;
icon?: React.ElementType;
}
type NavLink = BaseNavItem & {
url: LinkProps['to']
items?: never
}
url: LinkProps["to"];
items?: never;
};
type NavCollapsible = BaseNavItem & {
items: (BaseNavItem & { url: LinkProps['to'] })[]
url?: never
}
items: (BaseNavItem & { url: LinkProps["to"] })[];
url?: never;
};
type NavItem = NavCollapsible | NavLink
type NavItem = NavCollapsible | NavLink;
interface NavGroup {
title: string
items: NavItem[]
title: string;
items: NavItem[];
}
interface SidebarData {
user: User
teams: Team[]
navGroups: NavGroup[]
user: User;
teams: Team[];
navGroups: NavGroup[];
}
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink }
export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink };
+28 -28
View File
@@ -1,54 +1,54 @@
import { useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
PopoverTrigger
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
TooltipTrigger
} from "@/components/ui/tooltip";
interface Props {
children: React.ReactNode
className?: string
contentClassName?: string
children: React.ReactNode;
className?: string;
contentClassName?: string;
}
export default function LongText({
children,
className = '',
contentClassName = '',
className = "",
contentClassName = ""
}: Props) {
const ref = useRef<HTMLDivElement>(null)
const [isOverflown, setIsOverflown] = useState(false)
const ref = useRef<HTMLDivElement>(null);
const [isOverflown, setIsOverflown] = useState(false);
useEffect(() => {
if (checkOverflow(ref.current)) {
setIsOverflown(true)
return
setIsOverflown(true);
return;
}
setIsOverflown(false)
}, [])
setIsOverflown(false);
}, []);
if (!isOverflown)
return (
<div ref={ref} className={cn('truncate', className)}>
<div ref={ref} className={cn("truncate", className)}>
{children}
</div>
)
);
return (
<>
<div className='hidden sm:block'>
<div className="hidden sm:block">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<div ref={ref} className={cn('truncate', className)}>
<div ref={ref} className={cn("truncate", className)}>
{children}
</div>
</TooltipTrigger>
@@ -58,20 +58,20 @@ export default function LongText({
</Tooltip>
</TooltipProvider>
</div>
<div className='sm:hidden'>
<div className="sm:hidden">
<Popover>
<PopoverTrigger asChild>
<div ref={ref} className={cn('truncate', className)}>
<div ref={ref} className={cn("truncate", className)}>
{children}
</div>
</PopoverTrigger>
<PopoverContent className={cn('w-fit', contentClassName)}>
<PopoverContent className={cn("w-fit", contentClassName)}>
<p>{children}</p>
</PopoverContent>
</Popover>
</div>
</>
)
);
}
const checkOverflow = (textContainer: HTMLDivElement | null) => {
@@ -79,7 +79,7 @@ const checkOverflow = (textContainer: HTMLDivElement | null) => {
return (
textContainer.offsetHeight < textContainer.scrollHeight ||
textContainer.offsetWidth < textContainer.scrollWidth
)
);
}
return false
}
return false;
};
+17 -18
View File
@@ -1,12 +1,12 @@
"use client"
"use client";
import { ChevronRight, type LucideIcon } from "lucide-react"
import { ChevronRight, type LucideIcon } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
CollapsibleTrigger
} from "@/components/ui/collapsible";
import {
SidebarGroup,
SidebarGroupLabel,
@@ -15,22 +15,22 @@ import {
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
SidebarMenuSubItem
} from "@/components/ui/sidebar";
export function NavMain({
items,
items
}: {
items: {
title: string
url: string
icon?: LucideIcon
isActive?: boolean
title: string;
url: string;
icon?: LucideIcon;
isActive?: boolean;
items?: {
title: string
url: string
}[]
}[]
title: string;
url: string;
}[];
}[];
}) {
return (
<SidebarGroup>
@@ -41,8 +41,7 @@ export function NavMain({
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
className="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
@@ -69,5 +68,5 @@ export function NavMain({
))}
</SidebarMenu>
</SidebarGroup>
)
);
}
+15 -16
View File
@@ -1,20 +1,20 @@
"use client"
"use client";
import {
Folder,
Forward,
MoreHorizontal,
Trash2,
type LucideIcon,
} from "lucide-react"
type LucideIcon
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
@@ -22,19 +22,19 @@ import {
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
useSidebar
} from "@/components/ui/sidebar";
export function NavProjects({
projects,
projects
}: {
projects: {
name: string
url: string
icon: LucideIcon
}[]
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar()
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
@@ -58,8 +58,7 @@ export function NavProjects({
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
align={isMobile ? "end" : "start"}>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
@@ -85,5 +84,5 @@ export function NavProjects({
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
);
}
+17 -23
View File
@@ -1,4 +1,4 @@
"use client"
"use client";
import {
BadgeCheck,
@@ -6,14 +6,10 @@ import {
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from "lucide-react"
Sparkles
} from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
@@ -21,25 +17,25 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
useSidebar
} from "@/components/ui/sidebar";
export function NavUser({
user,
user
}: {
user: {
name: string
email: string
avatar: string
}
name: string;
email: string;
avatar: string;
};
}) {
const { isMobile } = useSidebar()
const { isMobile } = useSidebar();
return (
<SidebarMenu>
@@ -48,8 +44,7 @@ export function NavUser({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
@@ -65,8 +60,7 @@ export function NavUser({
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
sideOffset={4}>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
@@ -110,5 +104,5 @@ export function NavUser({
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
);
}
+19 -20
View File
@@ -1,39 +1,38 @@
import * as React from 'react'
import { IconEye, IconEyeOff } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { Button } from './ui/button'
import * as React from "react";
import { IconEye, IconEyeOff } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
type PasswordInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'type'
>
"type"
>;
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
({ className, disabled, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false)
const [showPassword, setShowPassword] = React.useState(false);
return (
<div className={cn('relative rounded-md', className)}>
<div className={cn("relative rounded-md", className)}>
<input
type={showPassword ? 'text' : 'password'}
className='flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50'
type={showPassword ? "text" : "password"}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
ref={ref}
disabled={disabled}
{...props}
/>
<Button
type='button'
size='icon'
variant='ghost'
type="button"
size="icon"
variant="ghost"
disabled={disabled}
className='absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground'
onClick={() => setShowPassword((prev) => !prev)}
>
className="absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground"
onClick={() => setShowPassword((prev) => !prev)}>
{showPassword ? <IconEye size={18} /> : <IconEyeOff size={18} />}
</Button>
</div>
)
);
}
)
PasswordInput.displayName = 'PasswordInput'
);
PasswordInput.displayName = "PasswordInput";
export { PasswordInput }
export { PasswordInput };
+159 -159
View File
@@ -1,79 +1,79 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
import * as React from "react";
import { cn } from "@/lib/utils";
interface PinInputProps {
children:
| React.ReactElement<typeof PinInputField>
| React.ReactElement<typeof PinInputField>[]
| React.ReactElement<typeof PinInputField>[];
/**
* className for the input container
*/
className?: string
className?: string;
/**
* `aria-label` for the input fields
*/
ariaLabel?: string
ariaLabel?: string;
/**
* If set, the pin input receives focus on mount, `false` by default
*/
autoFocus?: boolean
autoFocus?: boolean;
/**
* Called when value changes
*/
onChange?: (value: string) => void
onChange?: (value: string) => void;
/**
* Called when all inputs have valid value
*/
onComplete?: (value: string) => void
onComplete?: (value: string) => void;
/**
* Called when any input doesn't have value
*/
onIncomplete?: (value: string) => void
onIncomplete?: (value: string) => void;
/**
* `name` attribute for input fields
*/
name?: string
name?: string;
/**
* `form` attribute for hidden input
*/
form?: string
form?: string;
/**
* If set, the input's value will be masked just like password input. This field is `false` by default
*/
mask?: boolean
mask?: boolean;
/**
* If set, the pin input component signals to its fields that they should
* use `autocomplete="one-time-code"`. This field is `false` by default
*/
otp?: boolean
otp?: boolean;
/**
* Uncontrolled pin input default value.
*/
defaultValue?: string
defaultValue?: string;
/**
* Controlled pin input value.
*/
value?: string
value?: string;
/**
* The type of value pin input should allow, `alphanumeric` by default
*/
type?: 'numeric' | 'alphanumeric'
type?: "numeric" | "alphanumeric";
/**
* Placeholder for input fields, `○` by default
*/
placeholder?: string
placeholder?: string;
/**
* If set, the user cannot set the value, `false` by default
*/
readOnly?: boolean
readOnly?: boolean;
/**
* If set, the input fields are disabled, `false` by default
*/
disabled?: boolean
ref?: React.Ref<HTMLDivElement>
disabled?: boolean;
ref?: React.Ref<HTMLDivElement>;
}
const PinInputContext = React.createContext<boolean>(false)
const PinInputContext = React.createContext<boolean>(false);
const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
const {
@@ -82,8 +82,8 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
onChange,
onComplete,
onIncomplete,
placeholder = '○',
type = 'alphanumeric',
placeholder = "○",
type = "alphanumeric",
name,
form,
otp = false,
@@ -91,13 +91,13 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
disabled = false,
readOnly = false,
autoFocus = false,
ariaLabel = '',
ariaLabel = "",
...rest
} = props
} = props;
const validChildren = getValidChildren(children)
const validChildren = getValidChildren(children);
const length = getInputFieldCount(children)
const length = getInputFieldCount(children);
// pins, pinValue, refMap, ...handlers
const { pins, pinValue, refMap, ...handlers } = usePinInput({
@@ -106,47 +106,47 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
placeholder,
type,
length,
readOnly,
})
readOnly
});
/* call onChange func if pinValue changes */
React.useEffect(() => {
if (!onChange) return
onChange(pinValue)
}, [onChange, pinValue])
if (!onChange) return;
onChange(pinValue);
}, [onChange, pinValue]);
/* call onComplete/onIncomplete func if pinValue is valid and completed/incompleted */
const completeRef = React.useRef(pinValue.length === length)
const completeRef = React.useRef(pinValue.length === length);
React.useEffect(() => {
if (pinValue.length === length && completeRef.current === false) {
completeRef.current = true
if (onComplete) onComplete(pinValue)
completeRef.current = true;
if (onComplete) onComplete(pinValue);
}
if (pinValue.length !== length && completeRef.current === true) {
completeRef.current = false
if (onIncomplete) onIncomplete(pinValue)
completeRef.current = false;
if (onIncomplete) onIncomplete(pinValue);
}
}, [length, onComplete, onIncomplete, pinValue, pins, value])
}, [length, onComplete, onIncomplete, pinValue, pins, value]);
/* focus on first input field if autoFocus is set */
React.useEffect(() => {
if (!autoFocus) return
const node = refMap?.get(0)
if (!autoFocus) return;
const node = refMap?.get(0);
if (node) {
node.focus()
node.focus();
}
}, [autoFocus, refMap])
}, [autoFocus, refMap]);
const skipRef = React.useRef(0)
let counter = 0
const skipRef = React.useRef(0);
let counter = 0;
const clones = validChildren.map((child) => {
if (child.type === PinInputField) {
const pinIndex = counter
counter = counter + 1
const pinIndex = counter;
counter = counter + 1;
return React.cloneElement(child, {
name,
inputKey: `input-${pinIndex}`,
value: length > pinIndex ? pins[pinIndex] : '',
value: length > pinIndex ? pins[pinIndex] : "",
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
handlers.handleChange(e, pinIndex),
onFocus: (e: React.FocusEvent<HTMLInputElement>) =>
@@ -159,53 +159,53 @@ const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
placeholder: placeholder,
type: type,
mask: mask,
autoComplete: otp ? 'one-time-code' : 'off',
autoComplete: otp ? "one-time-code" : "off",
disabled: disabled,
readOnly: readOnly,
'aria-label': ariaLabel
"aria-label": ariaLabel
? ariaLabel
: `Pin input ${counter} of ${length}`,
ref: (node: HTMLInputElement | null) => {
if (node) {
refMap?.set(pinIndex, node)
refMap?.set(pinIndex, node);
} else {
refMap?.delete(pinIndex)
refMap?.delete(pinIndex);
}
},
})
}
skipRef.current = skipRef.current + 1
return child
})
});
}
skipRef.current = skipRef.current + 1;
return child;
});
return (
<PinInputContext.Provider value={true}>
<div ref={ref} aria-label='Pin Input' className={className} {...rest}>
<div ref={ref} aria-label="Pin Input" className={className} {...rest}>
{clones}
<input type='hidden' name={name} form={form} value={pinValue} />
<input type="hidden" name={name} form={form} value={pinValue} />
</div>
</PinInputContext.Provider>
)
}
PinInput.displayName = 'PinInput'
);
};
PinInput.displayName = "PinInput";
/* ========== PinInputField ========== */
interface _PinInputFieldProps {
mask: boolean
inputKey: string
type: 'numeric' | 'alphanumeric'
mask: boolean;
inputKey: string;
type: "numeric" | "alphanumeric";
}
interface PinInputFieldProps<T>
extends Omit<
React.ComponentPropsWithoutRef<'input'>,
React.ComponentPropsWithoutRef<"input">,
keyof _PinInputFieldProps
> {
component?: T
component?: T;
}
const PinInputField = <T extends React.ElementType = 'input'>({
const PinInputField = <T extends React.ElementType = "input">({
className,
component,
...props
@@ -214,38 +214,38 @@ const PinInputField = <T extends React.ElementType = 'input'>({
? never
: React.ComponentProps<T>)) => {
const { mask, type, inputKey, ...rest } = props as _PinInputFieldProps &
React.ComponentProps<T>
React.ComponentProps<T>;
// Check if PinInputField is used within PinInput
const isInsidePinInput = React.useContext(PinInputContext)
const isInsidePinInput = React.useContext(PinInputContext);
if (!isInsidePinInput) {
throw new Error(
`PinInputField must be used within ${PinInput.displayName}.`
)
);
}
const Element = component || 'input'
const Element = component || "input";
return (
<Element
key={inputKey}
type={mask ? 'password' : type === 'numeric' ? 'tel' : 'text'}
inputMode={type === 'numeric' ? 'numeric' : 'text'}
className={cn('size-10 text-center', className)}
type={mask ? "password" : type === "numeric" ? "tel" : "text"}
inputMode={type === "numeric" ? "numeric" : "text"}
className={cn("size-10 text-center", className)}
{...rest}
/>
)
}
);
};
/* ========== usePinInput custom hook ========== */
interface UsePinInputProps {
value: string | undefined
defaultValue: string | undefined
placeholder: string
type: 'numeric' | 'alphanumeric'
length: number
readOnly: boolean
value: string | undefined;
defaultValue: string | undefined;
placeholder: string;
type: "numeric" | "alphanumeric";
length: number;
readOnly: boolean;
}
const usePinInput = ({
@@ -254,7 +254,7 @@ const usePinInput = ({
placeholder,
type,
length,
readOnly,
readOnly
}: UsePinInputProps) => {
const pinInputs = React.useMemo(
() =>
@@ -263,43 +263,43 @@ const usePinInput = ({
? defaultValue.charAt(index)
: value
? value.charAt(index)
: ''
: ""
),
[defaultValue, length, value]
)
);
const [pins, setPins] = React.useState(pinInputs)
const pinValue = pins.join('').trim()
const [pins, setPins] = React.useState(pinInputs);
const pinValue = pins.join("").trim();
/**
* Update pins when values change programmatically.
* This syncs the pins if the `defaultValue` or `value` prop is updated.
*/
React.useEffect(() => {
setPins(pinInputs)
}, [pinInputs])
setPins(pinInputs);
}, [pinInputs]);
const itemsRef = React.useRef<Map<number, HTMLInputElement> | null>(null)
const itemsRef = React.useRef<Map<number, HTMLInputElement> | null>(null);
function getMap() {
if (!itemsRef.current) {
// Initialize the Map on first usage.
itemsRef.current = new Map()
itemsRef.current = new Map();
}
return itemsRef.current
return itemsRef.current;
}
function getNode(index: number) {
const map = getMap()
const node = map?.get(index)
return node
const map = getMap();
const node = map?.get(index);
return node;
}
function focusInput(itemId: number) {
const node = getNode(itemId)
const node = getNode(itemId);
if (node) {
node.focus()
node.placeholder = ''
node.focus();
node.placeholder = "";
}
}
@@ -307,116 +307,116 @@ const usePinInput = ({
event: React.FocusEvent<HTMLInputElement>,
index: number
) {
event.target.select()
focusInput(index)
event.target.select();
focusInput(index);
}
function handleBlur(index: number) {
const node = getNode(index)
const node = getNode(index);
if (node) {
node.placeholder = placeholder
node.placeholder = placeholder;
}
}
function updateInputField(val: string, index: number) {
const node = getNode(index)
const node = getNode(index);
if (node) {
node.value = val
node.value = val;
}
setPins((prev) =>
prev.map((p, i) => {
if (i === index) {
return val
return val;
} else {
return p
return p;
}
})
)
);
}
function validate(value: string) {
const NUMERIC_REGEX = /^[0-9]+$/
const ALPHA_NUMERIC_REGEX = /^[a-zA-Z0-9]+$/i
const regex = type === 'alphanumeric' ? ALPHA_NUMERIC_REGEX : NUMERIC_REGEX
return regex.test(value)
const NUMERIC_REGEX = /^[0-9]+$/;
const ALPHA_NUMERIC_REGEX = /^[a-zA-Z0-9]+$/i;
const regex = type === "alphanumeric" ? ALPHA_NUMERIC_REGEX : NUMERIC_REGEX;
return regex.test(value);
}
const pastedVal = React.useRef<null | string>(null)
const pastedVal = React.useRef<null | string>(null);
function handleChange(e: React.ChangeEvent<HTMLInputElement>, index: number) {
const inputValue = e.target.value
const pastedValue = pastedVal.current
const inputValue = e.target.value;
const pastedValue = pastedVal.current;
const inputChar =
pastedValue && pastedValue.length === length
? pastedValue.charAt(length - 1)
: inputValue.slice(-1)
: inputValue.slice(-1);
if (validate(inputChar)) {
updateInputField(inputChar, index)
pastedVal.current = null
updateInputField(inputChar, index);
pastedVal.current = null;
if (inputValue.length > 0) {
focusInput(index + 1)
focusInput(index + 1);
}
}
}
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
event.preventDefault()
event.preventDefault();
const copyValue = event.clipboardData
.getData('text/plain')
.replace(/[\n\r\s]+/g, '')
const copyArr = copyValue.split('').slice(0, length)
.getData("text/plain")
.replace(/[\n\r\s]+/g, "");
const copyArr = copyValue.split("").slice(0, length);
const isValid = copyArr.every((c) => validate(c))
const isValid = copyArr.every((c) => validate(c));
if (!isValid) return
if (!isValid) return;
for (let i = 0; i < length; i++) {
if (i < copyArr.length) {
updateInputField(copyArr[i], i)
updateInputField(copyArr[i], i);
}
}
pastedVal.current = copyValue
focusInput(copyArr.length < length ? copyArr.length : length - 1)
pastedVal.current = copyValue;
focusInput(copyArr.length < length ? copyArr.length : length - 1);
}
function handleKeyDown(
event: React.KeyboardEvent<HTMLInputElement>,
index: number
) {
const { ctrlKey, key, shiftKey, metaKey } = event
const { ctrlKey, key, shiftKey, metaKey } = event;
if (type === 'numeric') {
if (type === "numeric") {
const canTypeSign =
key === 'Backspace' ||
key === 'Tab' ||
key === 'Control' ||
key === 'Delete' ||
(ctrlKey && key === 'v') ||
(metaKey && key === 'v')
key === "Backspace" ||
key === "Tab" ||
key === "Control" ||
key === "Delete" ||
(ctrlKey && key === "v") ||
(metaKey && key === "v")
? true
: !Number.isNaN(Number(key))
: !Number.isNaN(Number(key));
if (!canTypeSign || readOnly) {
event.preventDefault()
event.preventDefault();
}
}
if (key === 'ArrowLeft' || (shiftKey && key === 'Tab')) {
event.preventDefault()
focusInput(index - 1)
} else if (key === 'ArrowRight' || key === 'Tab' || key === ' ') {
event.preventDefault()
focusInput(index + 1)
} else if (key === 'Delete') {
event.preventDefault()
} else if (key === 'Backspace') {
event.preventDefault()
updateInputField('', index)
if ((event.target as HTMLInputElement).value === '') {
focusInput(index - 1)
if (key === "ArrowLeft" || (shiftKey && key === "Tab")) {
event.preventDefault();
focusInput(index - 1);
} else if (key === "ArrowRight" || key === "Tab" || key === " ") {
event.preventDefault();
focusInput(index + 1);
} else if (key === "Delete") {
event.preventDefault();
} else if (key === "Backspace") {
event.preventDefault();
updateInputField("", index);
if ((event.target as HTMLInputElement).value === "") {
focusInput(index - 1);
}
}
}
@@ -429,26 +429,26 @@ const usePinInput = ({
handleBlur,
handleChange,
handlePaste,
handleKeyDown,
}
}
handleKeyDown
};
};
/* ========== Util Func ========== */
const getValidChildren = (children: React.ReactNode) =>
React.Children.toArray(children).filter((child) => {
if (React.isValidElement(child)) {
return React.isValidElement(child)
return React.isValidElement(child);
}
throw new Error(`${PinInput.displayName} contains invalid children.`)
throw new Error(`${PinInput.displayName} contains invalid children.`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as React.ReactElement<any>[]
}) as React.ReactElement<any>[];
const getInputFieldCount = (children: React.ReactNode) =>
React.Children.toArray(children).filter((child) => {
if (React.isValidElement(child) && child.type === PinInputField) {
return React.isValidElement(child)
return React.isValidElement(child);
}
}).length
}).length;
export { PinInput, PinInputField }
export { PinInput, PinInputField };
+17 -17
View File
@@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Link } from "@tanstack/react-router";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@@ -9,25 +9,25 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
export function ProfileDropdown() {
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
<Avatar className='h-8 w-8'>
<AvatarImage src='/avatars/01.png' alt='@shadcn' />
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src="/avatars/01.png" alt="@shadcn" />
<AvatarFallback>SN</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-56' align='end' forceMount>
<DropdownMenuLabel className='font-normal'>
<div className='flex flex-col space-y-1'>
<p className='text-sm font-medium leading-none'>satnaing</p>
<p className='text-xs leading-none text-muted-foreground'>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">satnaing</p>
<p className="text-xs leading-none text-muted-foreground">
satnaingdev@gmail.com
</p>
</div>
@@ -35,19 +35,19 @@ export function ProfileDropdown() {
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link to='/settings'>
<Link to="/settings">
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to='/settings'>
<Link to="/settings">
Billing
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to='/settings'>
<Link to="/settings">
Settings
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</Link>
@@ -61,5 +61,5 @@ export function ProfileDropdown() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
);
}
+18 -19
View File
@@ -1,33 +1,32 @@
import { IconSearch } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { useSearch } from '@/context/search-context'
import { Button } from './ui/button'
import { IconSearch } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { useSearch } from "@/context/search-context";
import { Button } from "./ui/button";
interface Props {
className?: string
type?: React.HTMLInputTypeAttribute
placeholder?: string
className?: string;
type?: React.HTMLInputTypeAttribute;
placeholder?: string;
}
export function Search({ className = '', placeholder = 'Search' }: Props) {
const { setOpen } = useSearch()
export function Search({ className = "", placeholder = "Search" }: Props) {
const { setOpen } = useSearch();
return (
<Button
variant='outline'
variant="outline"
className={cn(
'relative h-8 w-full flex-1 justify-start rounded-md bg-muted/25 text-sm font-normal text-muted-foreground shadow-none hover:bg-muted/50 sm:pr-12 md:w-40 md:flex-none lg:w-56 xl:w-64',
"relative h-8 w-full flex-1 justify-start rounded-md bg-muted/25 text-sm font-normal text-muted-foreground shadow-none hover:bg-muted/50 sm:pr-12 md:w-40 md:flex-none lg:w-56 xl:w-64",
className
)}
onClick={() => setOpen(true)}
>
onClick={() => setOpen(true)}>
<IconSearch
aria-hidden='true'
className='absolute left-1.5 top-1/2 -translate-y-1/2'
aria-hidden="true"
className="absolute left-1.5 top-1/2 -translate-y-1/2"
/>
<span className='ml-3'>{placeholder}</span>
<kbd className='pointer-events-none absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex'>
<span className='text-xs'>⌘</span>K
<span className="ml-3">{placeholder}</span>
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs">⌘</span>K
</kbd>
</Button>
)
);
}
+22 -22
View File
@@ -1,23 +1,23 @@
import { IconLoader } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { FormControl } from '@/components/ui/form'
import { IconLoader } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { FormControl } from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
SelectValue
} from "@/components/ui/select";
interface SelectDropdownProps {
onValueChange?: (value: string) => void
defaultValue: string | undefined
placeholder?: string
isPending?: boolean
items: { label: string; value: string }[] | undefined
disabled?: boolean
className?: string
isControlled?: boolean
onValueChange?: (value: string) => void;
defaultValue: string | undefined;
placeholder?: string;
isPending?: boolean;
items: { label: string; value: string }[] | undefined;
disabled?: boolean;
className?: string;
isControlled?: boolean;
}
export function SelectDropdown({
@@ -27,25 +27,25 @@ export function SelectDropdown({
items,
placeholder,
disabled,
className = '',
isControlled = false,
className = "",
isControlled = false
}: SelectDropdownProps) {
const defaultState = isControlled
? { value: defaultValue, onValueChange }
: { defaultValue, onValueChange }
: { defaultValue, onValueChange };
return (
<Select {...defaultState}>
<FormControl>
<SelectTrigger disabled={disabled} className={cn(className)}>
<SelectValue placeholder={placeholder ?? 'Select'} />
<SelectValue placeholder={placeholder ?? "Select"} />
</SelectTrigger>
</FormControl>
<SelectContent>
{isPending ? (
<SelectItem disabled value='loading' className='h-14'>
<div className='flex items-center justify-center gap-2'>
<IconLoader className='h-5 w-5 animate-spin' />
{' '}
<SelectItem disabled value="loading" className="h-14">
<div className="flex items-center justify-center gap-2">
<IconLoader className="h-5 w-5 animate-spin" />
{" "}
Loading...
</div>
</SelectItem>
@@ -58,5 +58,5 @@ export function SelectDropdown({
)}
</SelectContent>
</Select>
)
);
}
+4 -5
View File
@@ -2,11 +2,10 @@ const SkipToMain = () => {
return (
<a
className={`fixed left-44 z-[999] -translate-y-52 whitespace-nowrap bg-primary px-4 py-2 text-sm font-medium text-primary-foreground opacity-95 shadow transition hover:bg-primary/90 focus:translate-y-3 focus:transform focus-visible:ring-1 focus-visible:ring-ring`}
href='#content'
>
href="#content">
Skip to Main
</a>
)
}
);
};
export default SkipToMain
export default SkipToMain;
+19 -22
View File
@@ -1,7 +1,7 @@
"use client"
"use client";
import * as React from "react"
import { ChevronsUpDown, Plus } from "lucide-react"
import * as React from "react";
import { ChevronsUpDown, Plus } from "lucide-react";
import {
DropdownMenu,
@@ -10,26 +10,26 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
useSidebar
} from "@/components/ui/sidebar";
export function TeamSwitcher({
teams,
teams
}: {
teams: {
name: string
logo: React.ElementType
plan: string
}[]
name: string;
logo: React.ElementType;
plan: string;
}[];
}) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
const { isMobile } = useSidebar();
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
return (
<SidebarMenu>
@@ -38,9 +38,8 @@ export function TeamSwitcher({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
@@ -56,8 +55,7 @@ export function TeamSwitcher({
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
sideOffset={4}>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
@@ -65,8 +63,7 @@ export function TeamSwitcher({
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
@@ -85,5 +82,5 @@ export function TeamSwitcher({
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
);
}
+25 -25
View File
@@ -1,58 +1,58 @@
import { useEffect } from 'react'
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { useTheme } from '@/context/theme-context'
import { Button } from '@/components/ui/button'
import { useEffect } from "react";
import { IconCheck, IconMoon, IconSun } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { useTheme } from "@/context/theme-context";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
export function ThemeSwitch() {
const { theme, setTheme } = useTheme()
const { theme, setTheme } = useTheme();
/* Update theme-color meta tag
* when theme is updated */
useEffect(() => {
const themeColor = theme === 'dark' ? '#020817' : '#fff'
const metaThemeColor = document.querySelector("meta[name='theme-color']")
if (metaThemeColor) metaThemeColor.setAttribute('content', themeColor)
}, [theme])
const themeColor = theme === "dark" ? "#020817" : "#fff";
const metaThemeColor = document.querySelector("meta[name='theme-color']");
if (metaThemeColor) metaThemeColor.setAttribute("content", themeColor);
}, [theme]);
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='icon' className='scale-95 rounded-full'>
<IconSun className='size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
<IconMoon className='absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
<span className='sr-only'>Toggle theme</span>
<Button variant="ghost" size="icon" className="scale-95 rounded-full">
<IconSun className="size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<IconMoon className="absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => setTheme('light')}>
Light{' '}
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light{" "}
<IconCheck
size={14}
className={cn('ml-auto', theme !== 'light' && 'hidden')}
className={cn("ml-auto", theme !== "light" && "hidden")}
/>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
<IconCheck
size={14}
className={cn('ml-auto', theme !== 'dark' && 'hidden')}
className={cn("ml-auto", theme !== "dark" && "hidden")}
/>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
<IconCheck
size={14}
className={cn('ml-auto', theme !== 'system' && 'hidden')}
className={cn("ml-auto", theme !== "system" && "hidden")}
/>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
);
}
+33 -33
View File
@@ -1,13 +1,13 @@
import * as React from 'react'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
@@ -15,14 +15,14 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
@@ -33,14 +33,14 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
@@ -48,13 +48,13 @@ const AlertDialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = 'AlertDialogHeader'
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
className,
@@ -62,13 +62,13 @@ const AlertDialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = 'AlertDialogFooter'
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
@@ -76,11 +76,11 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
@@ -88,12 +88,12 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
@@ -104,8 +104,8 @@ const AlertDialogAction = React.forwardRef<
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
@@ -114,14 +114,14 @@ const AlertDialogCancel = React.forwardRef<
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
@@ -134,5 +134,5 @@ export {
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
AlertDialogCancel
};
+20 -20
View File
@@ -1,22 +1,22 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: 'bg-background text-foreground',
default: "bg-background text-foreground",
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
}
},
defaultVariants: {
variant: 'default',
},
variant: "default"
}
)
}
);
const Alert = React.forwardRef<
HTMLDivElement,
@@ -24,12 +24,12 @@ const Alert = React.forwardRef<
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role='alert'
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = 'Alert'
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
@@ -37,11 +37,11 @@ const AlertTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = 'AlertTitle'
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
@@ -49,10 +49,10 @@ const AlertDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = 'AlertDescription'
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription }
export { Alert, AlertTitle, AlertDescription };
+13 -13
View File
@@ -1,6 +1,6 @@
import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -9,13 +9,13 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -23,11 +23,11 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -36,12 +36,12 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback }
export { Avatar, AvatarImage, AvatarFallback };
+14 -14
View File
@@ -1,26 +1,26 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
},
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground"
}
},
defaultVariants: {
variant: 'default',
},
variant: "default"
}
)
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -29,7 +29,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };
+27 -29
View File
@@ -1,16 +1,16 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
@@ -24,8 +24,8 @@ const BreadcrumbList = React.forwardRef<
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
));
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
@@ -36,16 +36,16 @@ const BreadcrumbItem = React.forwardRef<
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
));
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
const Comp = asChild ? Slot : "a";
return (
<Comp
@@ -53,9 +53,9 @@ const BreadcrumbLink = React.forwardRef<
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
@@ -69,8 +69,8 @@ const BreadcrumbPage = React.forwardRef<
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
));
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({
children,
@@ -80,13 +80,12 @@ const BreadcrumbSeparator = ({
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props}>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({
className,
@@ -96,13 +95,12 @@ const BreadcrumbEllipsis = ({
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
{...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
@@ -111,5 +109,5 @@ export {
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
BreadcrumbEllipsis
};
+26 -26
View File
@@ -1,56 +1,56 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, 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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9"
}
},
defaultVariants: {
variant: 'default',
size: 'default',
},
variant: "default",
size: "default"
}
)
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
);
}
)
Button.displayName = 'Button'
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };
+40 -40
View File
@@ -1,10 +1,10 @@
import * as React from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { DayPicker } from 'react-day-picker'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
@@ -15,55 +15,55 @@ function Calendar({
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-3', className)}
className={cn("p-3", className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
props.mode === 'range'
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
: '[&:has([aria-selected])]:rounded-md'
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal aria-selected:opacity-100'
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: 'day-range-start',
day_range_end: 'day-range-end',
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
day_disabled: 'text-muted-foreground opacity-50',
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames
}}
components={{
IconLeft: () => <ChevronLeft className='h-4 w-4' />,
IconRight: () => <ChevronRight className='h-4 w-4' />,
IconLeft: () => <ChevronLeft className="h-4 w-4" />,
IconRight: () => <ChevronRight className="h-4 w-4" />
}}
{...props}
/>
)
);
}
Calendar.displayName = 'Calendar'
Calendar.displayName = "Calendar";
export { Calendar }
export { Calendar };
+28 -21
View File
@@ -1,5 +1,5 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
@@ -8,13 +8,13 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
'rounded-xl border bg-card text-card-foreground shadow',
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = 'Card'
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -22,11 +22,11 @@ const CardHeader = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = 'CardHeader'
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
@@ -34,11 +34,11 @@ const CardTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('font-semibold leading-none tracking-tight', className)}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = 'CardTitle'
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLDivElement,
@@ -46,19 +46,19 @@ const CardDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = 'CardDescription'
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
))
CardContent.displayName = 'CardContent'
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -66,10 +66,17 @@ const CardFooter = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = 'CardFooter'
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent
};
+11 -13
View File
@@ -1,7 +1,7 @@
import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { Check } from 'lucide-react'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -10,18 +10,16 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
{...props}>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className='h-4 w-4' />
className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }
export { Checkbox };
+5 -5
View File
@@ -1,9 +1,9 @@
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+41 -41
View File
@@ -1,15 +1,15 @@
import * as React from 'react'
import { type DialogProps } from '@radix-ui/react-dialog'
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
import { Command as CommandPrimitive } from 'cmdk'
import { Search } from 'lucide-react'
import { cn } from '@/lib/utils'
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from '@/components/ui/dialog'
DialogTitle
} from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
@@ -18,50 +18,50 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
));
Command.displayName = CommandPrimitive.displayName;
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className='overflow-hidden p-0'>
<DialogContent className="overflow-hidden p-0">
<VisuallyHidden asChild>
<DialogTitle />
</VisuallyHidden>
<VisuallyHidden asChild>
<DialogDescription />
</VisuallyHidden>
<Command className='[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'>
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
<Search className='mr-2 h-4 w-4 shrink-0 opacity-50' />
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
));
CommandInput.displayName = CommandPrimitive.Input.displayName
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
@@ -69,12 +69,12 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
));
CommandList.displayName = CommandPrimitive.List.displayName
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
@@ -82,12 +82,12 @@ const CommandEmpty = React.forwardRef<
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className='py-6 text-center text-sm'
className="py-6 text-center text-sm"
{...props}
/>
))
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
@@ -96,14 +96,14 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
));
CommandGroup.displayName = CommandPrimitive.Group.displayName
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
@@ -111,11 +111,11 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn('-mx-1 h-px bg-border', className)}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
@@ -124,14 +124,14 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props}
/>
))
));
CommandItem.displayName = CommandPrimitive.Item.displayName
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
@@ -140,14 +140,14 @@ const CommandShortcut = ({
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = 'CommandShortcut'
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
@@ -158,5 +158,5 @@ export {
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
CommandSeparator
};
+33 -34
View File
@@ -1,17 +1,17 @@
'use client'
"use client";
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -20,13 +20,13 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -37,20 +37,19 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{...props}>
{children}
<DialogPrimitive.Close className='absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground'>
<X className='h-4 w-4' />
<span className='sr-only'>Close</span>
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@@ -58,13 +57,13 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-1.5 text-center sm:text-left',
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = 'DialogHeader'
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
@@ -72,13 +71,13 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = 'DialogFooter'
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -87,13 +86,13 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
'text-lg font-semibold leading-none tracking-tight',
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@@ -101,11 +100,11 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@@ -117,5 +116,5 @@ export {
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
DialogDescription
};
+55 -58
View File
@@ -1,41 +1,40 @@
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check, ChevronRight, Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{...props}>
{children}
<ChevronRight className='ml-auto' />
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -44,14 +43,14 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -62,33 +61,33 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -97,22 +96,21 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className='h-4 w-4' />
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -121,38 +119,37 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className='h-2 w-2 fill-current' />
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -160,11 +157,11 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
@@ -172,12 +169,12 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
@@ -194,5 +191,5 @@ export {
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
DropdownMenuRadioGroup
};
+57 -57
View File
@@ -1,33 +1,33 @@
import * as React from 'react'
import * as React from "react";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from 'react-hook-form'
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'
useFormContext
} from "react-hook-form";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
@@ -35,21 +35,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState)
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>')
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext
const { id } = itemContext;
return {
id,
@@ -57,54 +57,55 @@ const useFormField = () => {
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
...fieldState
};
};
type FormItemContextValue = {
id: string
}
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = 'FormItem'
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && 'text-destructive', className)}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = 'FormLabel'
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
@@ -118,50 +119,49 @@ const FormControl = React.forwardRef<
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = 'FormControl'
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn('text-[0.8rem] text-muted-foreground', className)}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = 'FormDescription'
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-[0.8rem] font-medium text-destructive', className)}
{...props}
>
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}>
{body}
</p>
)
})
FormMessage.displayName = 'FormMessage'
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
@@ -171,5 +171,5 @@ export {
FormControl,
FormDescription,
FormMessage,
FormField,
}
FormField
};
+8 -8
View File
@@ -1,21 +1,21 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
import * as React from "react";
import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
);
}
)
Input.displayName = 'Input'
);
Input.displayName = "Input";
export { Input }
export { Input };
+9 -9
View File
@@ -1,11 +1,11 @@
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
)
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -17,7 +17,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };
+11 -11
View File
@@ -1,30 +1,30 @@
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
+16 -17
View File
@@ -1,7 +1,7 @@
import * as React from 'react'
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
import { Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
@@ -9,13 +9,13 @@ const RadioGroup = React.forwardRef<
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn('grid gap-2', className)}
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
);
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
@@ -25,17 +25,16 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className='flex items-center justify-center'>
<Circle className='h-3.5 w-3.5 fill-primary' />
{...props}>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem }
export { RadioGroup, RadioGroupItem };
+23 -26
View File
@@ -1,55 +1,52 @@
import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
interface ScrollAreaProps
extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
orientation?: 'horizontal' | 'vertical'
orientation?: "horizontal" | "vertical";
}
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
ScrollAreaProps
>(({ className, children, orientation = 'vertical', ...props }, ref) => (
>(({ className, children, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
className={cn("relative overflow-hidden", className)}
{...props}>
<ScrollAreaPrimitive.Viewport
className={cn(
'h-full w-full rounded-[inherit]',
orientation === 'horizontal' && '!overflow-x-auto'
)}
>
"h-full w-full rounded-[inherit]",
orientation === "horizontal" && "!overflow-x-auto"
)}>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar orientation={orientation} />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className='relative flex-1 rounded-full bg-border' />
{...props}>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar }
export { ScrollArea, ScrollBar };
+47 -53
View File
@@ -1,13 +1,13 @@
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -16,18 +16,17 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className='h-4 w-4 opacity-50' />
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@@ -36,15 +35,14 @@ const SelectScrollUpButton = React.forwardRef<
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className='h-4 w-4' />
{...props}>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@@ -53,48 +51,45 @@ const SelectScrollDownButton = React.forwardRef<
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
'flex cursor-default items-center justify-center py-1',
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className='h-4 w-4' />
{...props}>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
{...props}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
@@ -102,11 +97,11 @@ const SelectLabel = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
@@ -115,20 +110,19 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className='absolute right-2 flex h-3.5 w-3.5 items-center justify-center'>
{...props}>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className='h-4 w-4' />
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
@@ -136,11 +130,11 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
@@ -152,5 +146,5 @@ export {
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
SelectScrollDownButton
};
+9 -9
View File
@@ -1,13 +1,13 @@
import * as React from 'react'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = 'horizontal', decorative = true, ...props },
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
@@ -15,14 +15,14 @@ const Separator = React.forwardRef<
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator }
export { Separator };
+43 -44
View File
@@ -1,18 +1,18 @@
'use client'
"use client";
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
@@ -20,33 +20,33 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"
}
},
defaultVariants: {
side: 'right',
},
side: "right"
}
)
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
@@ -55,23 +55,22 @@ interface SheetContentProps
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className='absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary'>
<X className='h-4 w-4' />
<span className='sr-only'>Close</span>
{...props}>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
@@ -79,13 +78,13 @@ const SheetHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = 'SheetHeader'
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
@@ -93,13 +92,13 @@ const SheetFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = 'SheetFooter'
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
@@ -107,11 +106,11 @@ const SheetTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
@@ -119,11 +118,11 @@ const SheetDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
@@ -135,5 +134,5 @@ export {
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
SheetDescription
};
+36 -44
View File
@@ -12,14 +12,14 @@ import {
Sheet,
SheetContent,
SheetDescription,
SheetTitle,
SheetTitle
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
TooltipTrigger
} from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar:state";
@@ -127,7 +127,7 @@ const SidebarProvider = React.forwardRef<
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
toggleSidebar
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
);
@@ -140,16 +140,15 @@ const SidebarProvider = React.forwardRef<
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
...style
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
className
)}
ref={ref}
{...props}
>
{...props}>
{children}
</div>
</TooltipProvider>
@@ -184,12 +183,11 @@ const Sidebar = React.forwardRef<
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
className
)}
ref={ref}
{...props}
>
{...props}>
{children}
</div>
);
@@ -204,14 +202,13 @@ const Sidebar = React.forwardRef<
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
className="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
} as React.CSSProperties
}
side={side}
>
side={side}>
<VisuallyHidden asChild>
<SheetDescription />
</VisuallyHidden>
@@ -224,12 +221,11 @@ const Sidebar = React.forwardRef<
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
className="text-sidebar-foreground group peer hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
data-side={side}>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
@@ -253,12 +249,10 @@ const Sidebar = React.forwardRef<
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
{...props}>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow">
{children}
</div>
</div>
@@ -285,8 +279,7 @@ const SidebarTrigger = React.forwardRef<
onClick?.(event);
toggleSidebar();
}}
{...props}
>
{...props}>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
@@ -309,10 +302,10 @@ const SidebarRail = React.forwardRef<
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
@@ -350,7 +343,7 @@ const SidebarInput = React.forwardRef<
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
"focus-visible:ring-sidebar-ring h-8 w-full bg-background shadow-none focus-visible:ring-2",
className
)}
{...props}
@@ -397,7 +390,7 @@ const SidebarSeparator = React.forwardRef<
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
);
@@ -448,7 +441,7 @@ const SidebarGroupLabel = React.forwardRef<
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
@@ -469,7 +462,7 @@ const SidebarGroupAction = React.forwardRef<
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
@@ -527,18 +520,18 @@ const sidebarMenuButtonVariants = cva(
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))]",
"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",
},
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0"
}
},
defaultVariants: {
variant: "default",
size: "default",
},
size: "default"
}
}
);
@@ -582,7 +575,7 @@ const SidebarMenuButton = React.forwardRef<
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
children: tooltip
};
}
@@ -615,7 +608,7 @@ const SidebarMenuAction = React.forwardRef<
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
@@ -623,7 +616,7 @@ const SidebarMenuAction = React.forwardRef<
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
"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}
@@ -640,7 +633,7 @@ const SidebarMenuBadge = React.forwardRef<
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
@@ -669,8 +662,7 @@ const SidebarMenuSkeleton = React.forwardRef<
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{...props}>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
@@ -682,7 +674,7 @@ const SidebarMenuSkeleton = React.forwardRef<
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
"--skeleton-width": width
} as React.CSSProperties
}
/>
@@ -699,7 +691,7 @@ const SidebarMenuSub = React.forwardRef<
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
@@ -731,7 +723,7 @@ const SidebarMenuSubButton = React.forwardRef<
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>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-none 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",
@@ -768,5 +760,5 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
useSidebar
};
+4 -4
View File
@@ -1,4 +1,4 @@
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils";
function Skeleton({
className,
@@ -6,10 +6,10 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-md bg-primary/10', className)}
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
);
}
export { Skeleton }
export { Skeleton };
+9 -10
View File
@@ -1,6 +1,6 @@
import * as React from 'react'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -8,19 +8,18 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
ref={ref}>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch }
export { Switch };
+29 -29
View File
@@ -1,27 +1,27 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className='relative w-full overflow-auto'>
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn('w-full caption-bottom text-sm', className)}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = 'Table'
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
@@ -29,11 +29,11 @@ const TableBody = React.forwardRef<
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn('[&_tr:last-child]:border-0', className)}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = 'TableBody'
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
@@ -42,13 +42,13 @@ const TableFooter = React.forwardRef<
<tfoot
ref={ref}
className={cn(
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = 'TableFooter'
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
@@ -57,13 +57,13 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = 'TableRow'
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
@@ -72,13 +72,13 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
@@ -87,13 +87,13 @@ const TableCell = React.forwardRef<
<td
ref={ref}
className={cn(
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = 'TableCell'
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
@@ -101,11 +101,11 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn('mt-4 text-sm text-muted-foreground', className)}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = 'TableCaption'
));
TableCaption.displayName = "TableCaption";
export {
Table,
@@ -115,5 +115,5 @@ export {
TableHead,
TableRow,
TableCell,
TableCaption,
}
TableCaption
};
+14 -14
View File
@@ -1,8 +1,8 @@
import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
@@ -11,13 +11,13 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -26,13 +26,13 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@@ -41,12 +41,12 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };
+8 -8
View File
@@ -1,21 +1,21 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
import * as React from "react";
import { cn } from "@/lib/utils";
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<'textarea'>
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = 'Textarea'
);
});
Textarea.displayName = "Textarea";
export { Textarea }
export { Textarea };
+38 -39
View File
@@ -1,10 +1,10 @@
import * as React from 'react'
import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
@@ -13,29 +13,29 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: 'border bg-background text-foreground',
default: "border bg-background text-foreground",
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
"destructive group border-destructive bg-destructive text-destructive-foreground"
}
},
defaultVariants: {
variant: 'default',
},
variant: "default"
}
)
}
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
@@ -48,9 +48,9 @@ const Toast = React.forwardRef<
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
@@ -59,13 +59,13 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
@@ -74,16 +74,15 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=''
{...props}
>
<X className='h-4 w-4' />
toast-close=""
{...props}>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
@@ -91,11 +90,11 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
@@ -103,15 +102,15 @@ const ToastDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
@@ -122,5 +121,5 @@ export {
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
ToastAction
};
+7 -7
View File
@@ -1,22 +1,22 @@
import { useToast } from '@/hooks/use-toast'
import { useToast } from "@/hooks/use-toast";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from '@/components/ui/toast'
ToastViewport
} from "@/components/ui/toast";
export function Toaster() {
const { toasts } = useToast()
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className='grid gap-1'>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
@@ -25,9 +25,9 @@ export function Toaster() {
{action}
<ToastClose />
</Toast>
)
);
})}
<ToastViewport />
</ToastProvider>
)
);
}
+10 -10
View File
@@ -1,12 +1,12 @@
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from '@/lib/utils'
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -17,13 +17,13 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+1 -1
View File
@@ -25,4 +25,4 @@
* }
* ```
*/
export const fonts = ['inter', 'manrope', 'system'] as const
export const fonts = ["inter", "manrope", "system"] as const;
+26 -26
View File
@@ -1,48 +1,48 @@
import React, { createContext, useContext, useEffect, useState } from 'react'
import { fonts } from '@/config/fonts'
import React, { createContext, useContext, useEffect, useState } from "react";
import { fonts } from "@/config/fonts";
type Font = (typeof fonts)[number]
type Font = (typeof fonts)[number];
interface FontContextType {
font: Font
setFont: (font: Font) => void
font: Font;
setFont: (font: Font) => void;
}
const FontContext = createContext<FontContextType | undefined>(undefined)
const FontContext = createContext<FontContextType | undefined>(undefined);
export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
children,
children
}) => {
const [font, _setFont] = useState<Font>(() => {
const savedFont = localStorage.getItem('font')
return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
})
const savedFont = localStorage.getItem("font");
return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0];
});
useEffect(() => {
const applyFont = (font: string) => {
const root = document.documentElement
const root = document.documentElement;
root.classList.forEach((cls) => {
if (cls.startsWith('font-')) root.classList.remove(cls)
})
root.classList.add(`font-${font}`)
}
if (cls.startsWith("font-")) root.classList.remove(cls);
});
root.classList.add(`font-${font}`);
};
applyFont(font)
}, [font])
applyFont(font);
}, [font]);
const setFont = (font: Font) => {
localStorage.setItem('font', font)
_setFont(font)
}
localStorage.setItem("font", font);
_setFont(font);
};
return <FontContext value={{ font, setFont }}>{children}</FontContext>
}
return <FontContext value={{ font, setFont }}>{children}</FontContext>;
};
// eslint-disable-next-line react-refresh/only-export-components
export const useFont = () => {
const context = useContext(FontContext)
const context = useContext(FontContext);
if (!context) {
throw new Error('useFont must be used within a FontProvider')
throw new Error("useFont must be used within a FontProvider");
}
return context
}
return context;
};
+19 -19
View File
@@ -1,46 +1,46 @@
import React from 'react'
import { CommandMenu } from '@/components/command-menu'
import React from "react";
import { CommandMenu } from "@/components/command-menu";
interface SearchContextType {
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const SearchContext = React.createContext<SearchContextType | null>(null)
const SearchContext = React.createContext<SearchContextType | null>(null);
interface Props {
children: React.ReactNode
children: React.ReactNode;
}
export function SearchProvider({ children }: Props) {
const [open, setOpen] = React.useState(false)
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
return (
<SearchContext.Provider value={{ open, setOpen }}>
{children}
<CommandMenu />
</SearchContext.Provider>
)
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const useSearch = () => {
const searchContext = React.useContext(SearchContext)
const searchContext = React.useContext(SearchContext);
if (!searchContext) {
throw new Error('useSearch has to be used within <SearchContext.Provider>')
throw new Error("useSearch has to be used within <SearchContext.Provider>");
}
return searchContext
}
return searchContext;
};
+40 -40
View File
@@ -1,82 +1,82 @@
import { createContext, useContext, useEffect, useState } from 'react'
import { createContext, useContext, useEffect, useState } from "react";
type Theme = 'dark' | 'light' | 'system'
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null,
}
theme: "system",
setTheme: () => null
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, _setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
);
useEffect(() => {
const root = window.document.documentElement
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const root = window.document.documentElement;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const applyTheme = (theme: Theme) => {
root.classList.remove('light', 'dark') // Remove existing theme classes
const systemTheme = mediaQuery.matches ? 'dark' : 'light'
const effectiveTheme = theme === 'system' ? systemTheme : theme
root.classList.add(effectiveTheme) // Add the new theme class
}
root.classList.remove("light", "dark"); // Remove existing theme classes
const systemTheme = mediaQuery.matches ? "dark" : "light";
const effectiveTheme = theme === "system" ? systemTheme : theme;
root.classList.add(effectiveTheme); // Add the new theme class
};
const handleChange = () => {
if (theme === 'system') {
applyTheme('system')
}
if (theme === "system") {
applyTheme("system");
}
};
applyTheme(theme)
applyTheme(theme);
mediaQuery.addEventListener('change', handleChange)
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener('change', handleChange)
}, [theme])
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme]);
const setTheme = (theme: Theme) => {
localStorage.setItem(storageKey, theme)
_setTheme(theme)
}
localStorage.setItem(storageKey, theme);
_setTheme(theme);
};
const value = {
theme,
setTheme,
}
setTheme
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider')
throw new Error("useTheme must be used within a ThemeProvider");
return context
}
return context;
};
-110
View File
@@ -1,110 +0,0 @@
import {
IconBrandDiscord,
IconBrandDocker,
IconBrandFigma,
IconBrandGithub,
IconBrandGitlab,
IconBrandGmail,
IconBrandMedium,
IconBrandNotion,
IconBrandSkype,
IconBrandSlack,
IconBrandStripe,
IconBrandTelegram,
IconBrandTrello,
IconBrandWhatsapp,
IconBrandZoom,
} from '@tabler/icons-react'
export const apps = [
{
name: 'Telegram',
logo: <IconBrandTelegram />,
connected: false,
desc: 'Connect with Telegram for real-time communication.',
},
{
name: 'Notion',
logo: <IconBrandNotion />,
connected: true,
desc: 'Effortlessly sync Notion pages for seamless collaboration.',
},
{
name: 'Figma',
logo: <IconBrandFigma />,
connected: true,
desc: 'View and collaborate on Figma designs in one place.',
},
{
name: 'Trello',
logo: <IconBrandTrello />,
connected: false,
desc: 'Sync Trello cards for streamlined project management.',
},
{
name: 'Slack',
logo: <IconBrandSlack />,
connected: false,
desc: 'Integrate Slack for efficient team communication',
},
{
name: 'Zoom',
logo: <IconBrandZoom />,
connected: true,
desc: 'Host Zoom meetings directly from the dashboard.',
},
{
name: 'Stripe',
logo: <IconBrandStripe />,
connected: false,
desc: 'Easily manage Stripe transactions and payments.',
},
{
name: 'Gmail',
logo: <IconBrandGmail />,
connected: true,
desc: 'Access and manage Gmail messages effortlessly.',
},
{
name: 'Medium',
logo: <IconBrandMedium />,
connected: false,
desc: 'Explore and share Medium stories on your dashboard.',
},
{
name: 'Skype',
logo: <IconBrandSkype />,
connected: false,
desc: 'Connect with Skype contacts seamlessly.',
},
{
name: 'Docker',
logo: <IconBrandDocker />,
connected: false,
desc: 'Effortlessly manage Docker containers on your dashboard.',
},
{
name: 'GitHub',
logo: <IconBrandGithub />,
connected: false,
desc: 'Streamline code management with GitHub integration.',
},
{
name: 'GitLab',
logo: <IconBrandGitlab />,
connected: false,
desc: 'Efficiently manage code projects with GitLab integration.',
},
{
name: 'Discord',
logo: <IconBrandDiscord />,
connected: false,
desc: 'Connect with Discord for seamless team communication.',
},
{
name: 'WhatsApp',
logo: <IconBrandWhatsapp />,
connected: false,
desc: 'Easily integrate WhatsApp for direct messaging.',
},
]
-144
View File
@@ -1,144 +0,0 @@
import { useState } from 'react'
import {
IconAdjustmentsHorizontal,
IconSortAscendingLetters,
IconSortDescendingLetters,
} from '@tabler/icons-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Header } from '@/components/layout/header'
import { Main } from '@/components/layout/main'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { Search } from '@/components/search'
import { ThemeSwitch } from '@/components/theme-switch'
import { apps } from './data/apps'
const appText = new Map<string, string>([
['all', 'All Apps'],
['connected', 'Connected'],
['notConnected', 'Not Connected'],
])
export default function Apps() {
const [sort, setSort] = useState('ascending')
const [appType, setAppType] = useState('all')
const [searchTerm, setSearchTerm] = useState('')
const filteredApps = apps
.sort((a, b) =>
sort === 'ascending'
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
)
.filter((app) =>
appType === 'connected'
? app.connected
: appType === 'notConnected'
? !app.connected
: true
)
.filter((app) => app.name.toLowerCase().includes(searchTerm.toLowerCase()))
return (
<>
{/* ===== Top Heading ===== */}
<Header>
<Search />
<div className='ml-auto flex items-center gap-4'>
<ThemeSwitch />
<ProfileDropdown />
</div>
</Header>
{/* ===== Content ===== */}
<Main fixed>
<div>
<h1 className='text-2xl font-bold tracking-tight'>
App Integrations
</h1>
<p className='text-muted-foreground'>
Here&apos;s a list of your apps for the integration!
</p>
</div>
<div className='my-4 flex items-end justify-between sm:my-0 sm:items-center'>
<div className='flex flex-col gap-4 sm:my-4 sm:flex-row'>
<Input
placeholder='Filter apps...'
className='h-9 w-40 lg:w-[250px]'
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<Select value={appType} onValueChange={setAppType}>
<SelectTrigger className='w-36'>
<SelectValue>{appText.get(appType)}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>All Apps</SelectItem>
<SelectItem value='connected'>Connected</SelectItem>
<SelectItem value='notConnected'>Not Connected</SelectItem>
</SelectContent>
</Select>
</div>
<Select value={sort} onValueChange={setSort}>
<SelectTrigger className='w-16'>
<SelectValue>
<IconAdjustmentsHorizontal size={18} />
</SelectValue>
</SelectTrigger>
<SelectContent align='end'>
<SelectItem value='ascending'>
<div className='flex items-center gap-4'>
<IconSortAscendingLetters size={16} />
<span>Ascending</span>
</div>
</SelectItem>
<SelectItem value='descending'>
<div className='flex items-center gap-4'>
<IconSortDescendingLetters size={16} />
<span>Descending</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<Separator className='shadow' />
<ul className='faded-bottom no-scrollbar grid gap-4 overflow-auto pb-16 pt-4 md:grid-cols-2 lg:grid-cols-3'>
{filteredApps.map((app) => (
<li
key={app.name}
className='rounded-lg border p-4 hover:shadow-md'
>
<div className='mb-8 flex items-center justify-between'>
<div
className={`flex size-10 items-center justify-center rounded-lg bg-muted p-2`}
>
{app.logo}
</div>
<Button
variant='outline'
size='sm'
className={`${app.connected ? 'border border-blue-300 bg-blue-50 hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-950 dark:hover:bg-blue-900' : ''}`}
>
{app.connected ? 'Connected' : 'Connect'}
</Button>
</div>
<div>
<h2 className='mb-1 font-semibold'>{app.name}</h2>
<p className='line-clamp-2 text-gray-500'>{app.desc}</p>
</div>
</li>
))}
</ul>
</Main>
</>
)
}
@@ -1,28 +0,0 @@
interface Props {
children: React.ReactNode
}
export default function AuthLayout({ children }: Props) {
return (
<div className='container grid h-svh flex-col items-center justify-center bg-primary-foreground lg:max-w-none lg:px-0'>
<div className='mx-auto flex w-full flex-col justify-center space-y-2 sm:w-[480px] lg:p-8'>
<div className='mb-4 flex items-center justify-center'>
<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>
<h1 className='text-xl font-medium'>Shadcn Admin</h1>
</div>
{children}
</div>
</div>
)
}
@@ -1,70 +0,0 @@
import { HTMLAttributes, useState } from 'react'
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
type ForgotFormProps = HTMLAttributes<HTMLDivElement>
const formSchema = z.object({
email: z
.string()
.min(1, { message: 'Please enter your email' })
.email({ message: 'Invalid email address' }),
})
export function ForgotForm({ className, ...props }: ForgotFormProps) {
const [isLoading, setIsLoading] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: '' },
})
function onSubmit(data: z.infer<typeof formSchema>) {
setIsLoading(true)
// eslint-disable-next-line no-console
console.log(data)
setTimeout(() => {
setIsLoading(false)
}, 3000)
}
return (
<div className={cn('grid gap-6', className)} {...props}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='grid gap-2'>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem className='space-y-1'>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder='name@example.com' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className='mt-2' disabled={isLoading}>
Continue
</Button>
</div>
</form>
</Form>
</div>
)
}
@@ -1,33 +0,0 @@
import { Link } from '@tanstack/react-router'
import { Card } from '@/components/ui/card'
import AuthLayout from '../auth-layout'
import { ForgotForm } from './components/forgot-password-form'
export default function ForgotPassword() {
return (
<AuthLayout>
<Card className='p-6'>
<div className='mb-2 flex flex-col space-y-2 text-left'>
<h1 className='text-md font-semibold tracking-tight'>
Forgot Password
</h1>
<p className='text-sm text-muted-foreground'>
Enter your registered email and <br /> we will send you a link to
reset your password.
</p>
</div>
<ForgotForm />
<p className='mt-4 px-8 text-center text-sm text-muted-foreground'>
Don't have an account?{' '}
<Link
to='/sign-up'
className='underline underline-offset-4 hover:text-primary'
>
Sign up
</Link>
.
</p>
</Card>
</AuthLayout>
)
}
@@ -1,95 +0,0 @@
import { HTMLAttributes, useState } from 'react'
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useNavigate } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
import { PinInput, PinInputField } from '@/components/pin-input'
type OtpFormProps = HTMLAttributes<HTMLDivElement>
const formSchema = z.object({
otp: z.string().min(1, { message: 'Please enter your otp code.' }),
})
export function OtpForm({ className, ...props }: OtpFormProps) {
const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(false)
const [disabledBtn, setDisabledBtn] = useState(true)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { otp: '' },
})
function onSubmit(data: z.infer<typeof formSchema>) {
setIsLoading(true)
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
setTimeout(() => {
setIsLoading(false)
navigate({ to: '/' })
}, 1000)
}
return (
<div className={cn('grid gap-6', className)} {...props}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='grid gap-2'>
<FormField
control={form.control}
name='otp'
render={({ field }) => (
<FormItem className='space-y-1'>
<FormControl>
<PinInput
{...field}
className='flex h-10 justify-between'
onComplete={() => setDisabledBtn(false)}
onIncomplete={() => setDisabledBtn(true)}
>
{Array.from({ length: 7 }, (_, i) => {
if (i === 3)
return <Separator key={i} orientation='vertical' />
return (
<PinInputField
key={i}
component={Input}
className={`${form.getFieldState('otp').invalid ? 'border-red-500' : ''}`}
/>
)
})}
</PinInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className='mt-2' disabled={disabledBtn || isLoading}>
Verify
</Button>
</div>
</form>
</Form>
</div>
)
}
-33
View File
@@ -1,33 +0,0 @@
import { Link } from '@tanstack/react-router'
import { Card } from '@/components/ui/card'
import AuthLayout from '../auth-layout'
import { OtpForm } from './components/otp-form'
export default function Otp() {
return (
<AuthLayout>
<Card className='p-6'>
<div className='mb-2 flex flex-col space-y-2 text-left'>
<h1 className='text-md font-semibold tracking-tight'>
Two-factor Authentication
</h1>
<p className='text-sm text-muted-foreground'>
Please enter the authentication code. <br /> We have sent the
authentication code to your email.
</p>
</div>
<OtpForm />
<p className='mt-4 px-8 text-center text-sm text-muted-foreground'>
Haven't received it?{' '}
<Link
to='/sign-in'
className='underline underline-offset-4 hover:text-primary'
>
Resend a new code.
</Link>
.
</p>
</Card>
</AuthLayout>
)
}
@@ -1,135 +0,0 @@
import { HTMLAttributes, useState } from 'react'
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Link } from '@tanstack/react-router'
import { IconBrandFacebook, IconBrandGithub } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { PasswordInput } from '@/components/password-input'
type UserAuthFormProps = HTMLAttributes<HTMLDivElement>
const formSchema = z.object({
email: z
.string()
.min(1, { message: 'Please enter your email' })
.email({ message: 'Invalid email address' }),
password: z
.string()
.min(1, {
message: 'Please enter your password',
})
.min(7, {
message: 'Password must be at least 7 characters long',
}),
})
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const [isLoading, setIsLoading] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
setIsLoading(true)
// eslint-disable-next-line no-console
console.log(data)
setTimeout(() => {
setIsLoading(false)
}, 3000)
}
return (
<div className={cn('grid gap-6', className)} {...props}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='grid gap-2'>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem className='space-y-1'>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder='name@example.com' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({ field }) => (
<FormItem className='space-y-1'>
<div className='flex items-center justify-between'>
<FormLabel>Password</FormLabel>
<Link
to='/forgot-password'
className='text-sm font-medium text-muted-foreground hover:opacity-75'
>
Forgot password?
</Link>
</div>
<FormControl>
<PasswordInput placeholder='********' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className='mt-2' disabled={isLoading}>
Login
</Button>
<div className='relative my-2'>
<div className='absolute inset-0 flex items-center'>
<span className='w-full border-t' />
</div>
<div className='relative flex justify-center text-xs uppercase'>
<span className='bg-background px-2 text-muted-foreground'>
Or continue with
</span>
</div>
</div>
<div className='flex items-center gap-2'>
<Button
variant='outline'
className='w-full'
type='button'
disabled={isLoading}
>
<IconBrandGithub className='h-4 w-4' /> GitHub
</Button>
<Button
variant='outline'
className='w-full'
type='button'
disabled={isLoading}
>
<IconBrandFacebook className='h-4 w-4' /> Facebook
</Button>
</div>
</div>
</form>
</Form>
</div>
)
}
@@ -1,37 +0,0 @@
import { Card } from '@/components/ui/card'
import AuthLayout from '../auth-layout'
import { UserAuthForm } from './components/user-auth-form'
export default function SignIn() {
return (
<AuthLayout>
<Card className='p-6'>
<div className='flex flex-col space-y-2 text-left'>
<h1 className='text-2xl font-semibold tracking-tight'>Login</h1>
<p className='text-sm text-muted-foreground'>
Enter your email and password below <br />
to log into your account
</p>
</div>
<UserAuthForm />
<p className='mt-4 px-8 text-center text-sm text-muted-foreground'>
By clicking login, you agree to our{' '}
<a
href='/terms'
className='underline underline-offset-4 hover:text-primary'
>
Terms of Service
</a>{' '}
and{' '}
<a
href='/privacy'
className='underline underline-offset-4 hover:text-primary'
>
Privacy Policy
</a>
.
</p>
</Card>
</AuthLayout>
)
}
@@ -1,75 +0,0 @@
import ViteLogo from '@/assets/vite.svg'
import { UserAuthForm } from './components/user-auth-form'
export default function SignIn2() {
return (
<div className='container relative grid h-svh flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0'>
<div className='relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex'>
<div className='absolute inset-0 bg-zinc-900' />
<div 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
</div>
<img
src={ViteLogo}
className='relative m-auto'
width={301}
height={60}
alt='Vite'
/>
<div className='relative z-20 mt-auto'>
<blockquote className='space-y-2'>
<p className='text-lg'>
&ldquo;This template has saved me countless hours of work and
helped me deliver stunning designs to my clients faster than ever
before.&rdquo;
</p>
<footer className='text-sm'>John Doe</footer>
</blockquote>
</div>
</div>
<div className='lg:p-8'>
<div className='mx-auto flex w-full flex-col justify-center space-y-2 sm:w-[350px]'>
<div className='flex flex-col space-y-2 text-left'>
<h1 className='text-2xl font-semibold tracking-tight'>Login</h1>
<p className='text-sm text-muted-foreground'>
Enter your email and password below <br />
to log into your account
</p>
</div>
<UserAuthForm />
<p className='px-8 text-center text-sm text-muted-foreground'>
By clicking login, you agree to our{' '}
<a
href='/terms'
className='underline underline-offset-4 hover:text-primary'
>
Terms of Service
</a>{' '}
and{' '}
<a
href='/privacy'
className='underline underline-offset-4 hover:text-primary'
>
Privacy Policy
</a>
.
</p>
</div>
</div>
</div>
)
}
@@ -1,146 +0,0 @@
import { HTMLAttributes, useState } from 'react'
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { IconBrandFacebook, IconBrandGithub } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { PasswordInput } from '@/components/password-input'
type SignUpFormProps = HTMLAttributes<HTMLDivElement>
const formSchema = z
.object({
email: z
.string()
.min(1, { message: 'Please enter your email' })
.email({ message: 'Invalid email address' }),
password: z
.string()
.min(1, {
message: 'Please enter your password',
})
.min(7, {
message: 'Password must be at least 7 characters long',
}),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match.",
path: ['confirmPassword'],
})
export function SignUpForm({ className, ...props }: SignUpFormProps) {
const [isLoading, setIsLoading] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
confirmPassword: '',
},
})
function onSubmit(data: z.infer<typeof formSchema>) {
setIsLoading(true)
// eslint-disable-next-line no-console
console.log(data)
setTimeout(() => {
setIsLoading(false)
}, 3000)
}
return (
<div className={cn('grid gap-6', className)} {...props}>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='grid gap-2'>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem className='space-y-1'>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder='name@example.com' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({ field }) => (
<FormItem className='space-y-1'>
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput placeholder='********' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='confirmPassword'
render={({ field }) => (
<FormItem className='space-y-1'>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<PasswordInput placeholder='********' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className='mt-2' disabled={isLoading}>
Create Account
</Button>
<div className='relative my-2'>
<div className='absolute inset-0 flex items-center'>
<span className='w-full border-t' />
</div>
<div className='relative flex justify-center text-xs uppercase'>
<span className='bg-background px-2 text-muted-foreground'>
Or continue with
</span>
</div>
</div>
<div className='flex items-center gap-2'>
<Button
variant='outline'
className='w-full'
type='button'
disabled={isLoading}
>
<IconBrandGithub className='h-4 w-4' /> GitHub
</Button>
<Button
variant='outline'
className='w-full'
type='button'
disabled={isLoading}
>
<IconBrandFacebook className='h-4 w-4' /> Facebook
</Button>
</div>
</div>
</form>
</Form>
</div>
)
}
@@ -1,46 +0,0 @@
import { Link } from '@tanstack/react-router'
import { Card } from '@/components/ui/card'
import AuthLayout from '../auth-layout'
import { SignUpForm } from './components/sign-up-form'
export default function SignUp() {
return (
<AuthLayout>
<Card className='p-6'>
<div className='mb-2 flex flex-col space-y-2 text-left'>
<h1 className='text-lg font-semibold tracking-tight'>
Create an account
</h1>
<p className='text-sm text-muted-foreground'>
Enter your email and password to create an account. <br />
Already have an account?{' '}
<Link
to='/sign-in'
className='underline underline-offset-4 hover:text-primary'
>
Sign In
</Link>
</p>
</div>
<SignUpForm />
<p className='mt-4 px-8 text-center text-sm text-muted-foreground'>
By creating an account, you agree to our{' '}
<a
href='/terms'
className='underline underline-offset-4 hover:text-primary'
>
Terms of Service
</a>{' '}
and{' '}
<a
href='/privacy'
className='underline underline-offset-4 hover:text-primary'
>
Privacy Policy
</a>
.
</p>
</Card>
</AuthLayout>
)
}
@@ -1,138 +0,0 @@
import { useEffect, useState } from 'react'
import { IconCheck, IconX } from '@tabler/icons-react'
import { toast } from '@/hooks/use-toast'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ChatUser } from '../data/chat-types'
type User = Omit<ChatUser, 'messages'>
type Props = {
users: User[]
open: boolean
onOpenChange: (open: boolean) => void
}
export function NewChat({ users, onOpenChange, open }: Props) {
const [selectedUsers, setSelectedUsers] = useState<User[]>([])
const handleSelectUser = (user: User) => {
if (!selectedUsers.find((u) => u.id === user.id)) {
setSelectedUsers([...selectedUsers, user])
} else {
handleRemoveUser(user.id)
}
}
const handleRemoveUser = (userId: string) => {
setSelectedUsers(selectedUsers.filter((user) => user.id !== userId))
}
useEffect(() => {
if (!open) {
setSelectedUsers([])
}
}, [open])
const onSubmit = () => {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>
{JSON.stringify(selectedUsers, null, 2)}
</code>
</pre>
),
})
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-[600px]'>
<DialogHeader>
<DialogTitle>New message</DialogTitle>
</DialogHeader>
<div className='flex flex-col gap-4'>
<div className='flex flex-wrap items-center gap-2'>
<span className='text-sm text-zinc-400'>To:</span>
{selectedUsers.map((user) => (
<Badge key={user.id} variant='default'>
{user.fullName}
<button
className='ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2'
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRemoveUser(user.id)
}
}}
onClick={() => handleRemoveUser(user.id)}
>
<IconX className='h-3 w-3 text-muted-foreground hover:text-foreground' />
</button>
</Badge>
))}
</div>
<Command className='rounded-lg border'>
<CommandInput
placeholder='Search people...'
className='text-foreground'
/>
<CommandList>
<CommandEmpty>No people found.</CommandEmpty>
<CommandGroup>
{users.map((user) => (
<CommandItem
key={user.id}
onSelect={() => handleSelectUser(user)}
className='flex items-center justify-between gap-2'
>
<div className='flex items-center gap-2'>
<img
src={user.profile || '/placeholder.svg'}
alt={user.fullName}
className='h-8 w-8 rounded-full'
/>
<div className='flex flex-col'>
<span className='text-sm font-medium'>
{user.fullName}
</span>
<span className='text-xs text-zinc-400'>
{user.username}
</span>
</div>
</div>
{selectedUsers.find((u) => u.id === user.id) && (
<IconCheck className='h-4 w-4' />
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
<Button
variant={'default'}
onClick={onSubmit}
disabled={selectedUsers.length === 0}
>
Chat
</Button>
</div>
</DialogContent>
</Dialog>
)
}
@@ -1,4 +0,0 @@
import { conversations } from './convo.json'
export type ChatUser = (typeof conversations)[number]
export type Convo = ChatUser['messages'][number]
-346
View File
@@ -1,346 +0,0 @@
import { useState } from 'react'
import { Fragment } from 'react/jsx-runtime'
import { format } from 'date-fns'
import {
IconArrowLeft,
IconDotsVertical,
IconEdit,
IconMessages,
IconPaperclip,
IconPhone,
IconPhotoPlus,
IconPlus,
IconSearch,
IconSend,
IconVideo,
} from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Header } from '@/components/layout/header'
import { Main } from '@/components/layout/main'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { Search } from '@/components/search'
import { ThemeSwitch } from '@/components/theme-switch'
import { NewChat } from './components/new-chat'
import { type ChatUser, type Convo } from './data/chat-types'
// Fake Data
import { conversations } from './data/convo.json'
export default function Chats() {
const [search, setSearch] = useState('')
const [selectedUser, setSelectedUser] = useState<ChatUser | null>(null)
const [mobileSelectedUser, setMobileSelectedUser] = useState<ChatUser | null>(
null
)
const [createConversationDialogOpened, setCreateConversationDialog] =
useState(false)
// Filtered data based on the search query
const filteredChatList = conversations.filter(({ fullName }) =>
fullName.toLowerCase().includes(search.trim().toLowerCase())
)
const currentMessage = selectedUser?.messages.reduce(
(acc: Record<string, Convo[]>, obj) => {
const key = format(obj.timestamp, 'd MMM, yyyy')
// Create an array for the category if it doesn't exist
if (!acc[key]) {
acc[key] = []
}
// Push the current object to the array
acc[key].push(obj)
return acc
},
{}
)
const users = conversations.map(({ messages, ...user }) => user)
return (
<>
{/* ===== Top Heading ===== */}
<Header>
<Search />
<div className='ml-auto flex items-center space-x-4'>
<ThemeSwitch />
<ProfileDropdown />
</div>
</Header>
<Main fixed>
<section className='flex h-full gap-6'>
{/* Left Side */}
<div className='flex w-full flex-col gap-2 sm:w-56 lg:w-72 2xl:w-80'>
<div className='sticky top-0 z-10 -mx-4 bg-background px-4 pb-3 shadow-md sm:static sm:z-auto sm:mx-0 sm:p-0 sm:shadow-none'>
<div className='flex items-center justify-between py-2'>
<div className='flex gap-2'>
<h1 className='text-2xl font-bold'>Inbox</h1>
<IconMessages size={20} />
</div>
<Button
size='icon'
variant='ghost'
onClick={() => setCreateConversationDialog(true)}
className='rounded-lg'
>
<IconEdit size={24} className='stroke-muted-foreground' />
</Button>
</div>
<label className='flex h-12 w-full items-center space-x-0 rounded-md border border-input pl-2 focus-within:outline-none focus-within:ring-1 focus-within:ring-ring'>
<IconSearch size={15} className='mr-2 stroke-slate-500' />
<span className='sr-only'>Search</span>
<input
type='text'
className='w-full flex-1 bg-inherit text-sm focus-visible:outline-none'
placeholder='Search chat...'
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</label>
</div>
<ScrollArea className='-mx-3 h-full p-3'>
{filteredChatList.map((chatUsr) => {
const { id, profile, username, messages, fullName } = chatUsr
const lastConvo = messages[0]
const lastMsg =
lastConvo.sender === 'You'
? `You: ${lastConvo.message}`
: lastConvo.message
return (
<Fragment key={id}>
<button
type='button'
className={cn(
`-mx-1 flex w-full rounded-md px-2 py-2 text-left text-sm hover:bg-secondary/75`,
selectedUser?.id === id && 'sm:bg-muted'
)}
onClick={() => {
setSelectedUser(chatUsr)
setMobileSelectedUser(chatUsr)
}}
>
<div className='flex gap-2'>
<Avatar>
<AvatarImage src={profile} alt={username} />
<AvatarFallback>{username}</AvatarFallback>
</Avatar>
<div>
<span className='col-start-2 row-span-2 font-medium'>
{fullName}
</span>
<span className='col-start-2 row-span-2 row-start-2 line-clamp-2 text-ellipsis text-muted-foreground'>
{lastMsg}
</span>
</div>
</div>
</button>
<Separator className='my-1' />
</Fragment>
)
})}
</ScrollArea>
</div>
{/* Right Side */}
{selectedUser ? (
<div
className={cn(
'absolute inset-0 left-full z-50 hidden w-full flex-1 flex-col rounded-md border bg-primary-foreground shadow-sm transition-all duration-200 sm:static sm:z-auto sm:flex',
mobileSelectedUser && 'left-0 flex'
)}
>
{/* Top Part */}
<div className='mb-1 flex flex-none justify-between rounded-t-md bg-secondary p-4 shadow-lg'>
{/* Left */}
<div className='flex gap-3'>
<Button
size='icon'
variant='ghost'
className='-ml-2 h-full sm:hidden'
onClick={() => setMobileSelectedUser(null)}
>
<IconArrowLeft />
</Button>
<div className='flex items-center gap-2 lg:gap-4'>
<Avatar className='size-9 lg:size-11'>
<AvatarImage
src={selectedUser.profile}
alt={selectedUser.username}
/>
<AvatarFallback>{selectedUser.username}</AvatarFallback>
</Avatar>
<div>
<span className='col-start-2 row-span-2 text-sm font-medium lg:text-base'>
{selectedUser.fullName}
</span>
<span className='col-start-2 row-span-2 row-start-2 line-clamp-1 block max-w-32 text-ellipsis text-nowrap text-xs text-muted-foreground lg:max-w-none lg:text-sm'>
{selectedUser.title}
</span>
</div>
</div>
</div>
{/* Right */}
<div className='-mr-1 flex items-center gap-1 lg:gap-2'>
<Button
size='icon'
variant='ghost'
className='hidden size-8 rounded-full sm:inline-flex lg:size-10'
>
<IconVideo size={22} className='stroke-muted-foreground' />
</Button>
<Button
size='icon'
variant='ghost'
className='hidden size-8 rounded-full sm:inline-flex lg:size-10'
>
<IconPhone size={22} className='stroke-muted-foreground' />
</Button>
<Button
size='icon'
variant='ghost'
className='h-10 rounded-md sm:h-8 sm:w-4 lg:h-10 lg:w-6'
>
<IconDotsVertical className='stroke-muted-foreground sm:size-5' />
</Button>
</div>
</div>
{/* Conversation */}
<div className='flex flex-1 flex-col gap-2 rounded-md px-4 pb-4 pt-0'>
<div className='flex size-full flex-1'>
<div className='chat-text-container relative -mr-4 flex flex-1 flex-col overflow-y-hidden'>
<div className='chat-flex flex h-40 w-full flex-grow flex-col-reverse justify-start gap-4 overflow-y-auto py-2 pb-4 pr-4'>
{currentMessage &&
Object.keys(currentMessage).map((key) => (
<Fragment key={key}>
{currentMessage[key].map((msg, index) => (
<div
key={`${msg.sender}-${msg.timestamp}-${index}`}
className={cn(
'chat-box max-w-72 break-words px-3 py-2 shadow-lg',
msg.sender === 'You'
? 'self-end rounded-[16px_16px_0_16px] bg-primary/85 text-primary-foreground/75'
: 'self-start rounded-[16px_16px_16px_0] bg-secondary'
)}
>
{msg.message}{' '}
<span
className={cn(
'mt-1 block text-xs font-light italic text-muted-foreground',
msg.sender === 'You' && 'text-right'
)}
>
{format(msg.timestamp, 'h:mm a')}
</span>
</div>
))}
<div className='text-center text-xs'>{key}</div>
</Fragment>
))}
</div>
</div>
</div>
<form className='flex w-full flex-none gap-2'>
<div className='flex flex-1 items-center gap-2 rounded-md border border-input px-2 py-1 focus-within:outline-none focus-within:ring-1 focus-within:ring-ring lg:gap-4'>
<div className='space-x-1'>
<Button
size='icon'
type='button'
variant='ghost'
className='h-8 rounded-md'
>
<IconPlus
size={20}
className='stroke-muted-foreground'
/>
</Button>
<Button
size='icon'
type='button'
variant='ghost'
className='hidden h-8 rounded-md lg:inline-flex'
>
<IconPhotoPlus
size={20}
className='stroke-muted-foreground'
/>
</Button>
<Button
size='icon'
type='button'
variant='ghost'
className='hidden h-8 rounded-md lg:inline-flex'
>
<IconPaperclip
size={20}
className='stroke-muted-foreground'
/>
</Button>
</div>
<label className='flex-1'>
<span className='sr-only'>Chat Text Box</span>
<input
type='text'
placeholder='Type your messages...'
className='h-8 w-full bg-inherit focus-visible:outline-none'
/>
</label>
<Button
variant='ghost'
size='icon'
className='hidden sm:inline-flex'
>
<IconSend size={20} />
</Button>
</div>
<Button className='h-full sm:hidden'>
<IconSend size={18} /> Send
</Button>
</form>
</div>
</div>
) : (
<div
className={cn(
'absolute inset-0 left-full z-50 hidden w-full flex-1 flex-col justify-center rounded-md border bg-primary-foreground shadow-sm transition-all duration-200 sm:static sm:z-auto sm:flex'
)}
>
<div className='flex flex-col items-center space-y-6'>
<div className='flex h-16 w-16 items-center justify-center rounded-full border-2 border-white'>
<IconMessages className='h-8 w-8' />
</div>
<div className='space-y-2 text-center'>
<h1 className='text-xl font-semibold'>Your messages</h1>
<p className='text-sm text-gray-400'>
Send a message to start a chat.
</p>
</div>
<Button
className='bg-blue-500 px-6 text-white hover:bg-blue-600'
onClick={() => setCreateConversationDialog(true)}
>
Send message
</Button>
</div>
</div>
)}
</section>
<NewChat
users={users}
onOpenChange={setCreateConversationDialog}
open={createConversationDialogOpened}
/>
</Main>
</>
)
}
@@ -1,81 +0,0 @@
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'
const data = [
{
name: 'Jan',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Feb',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Mar',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Apr',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'May',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Jun',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Jul',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Aug',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Sep',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Oct',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Nov',
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: 'Dec',
total: Math.floor(Math.random() * 5000) + 1000,
},
]
export function Overview() {
return (
<ResponsiveContainer width='100%' height={350}>
<BarChart data={data}>
<XAxis
dataKey='name'
stroke='#888888'
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke='#888888'
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `$${value}`}
/>
<Bar
dataKey='total'
fill='currentColor'
radius={[4, 4, 0, 0]}
className='fill-primary'
/>
</BarChart>
</ResponsiveContainer>
)
}
@@ -1,83 +0,0 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
export function RecentSales() {
return (
<div className='space-y-8'>
<div className='flex items-center gap-4'>
<Avatar className='h-9 w-9'>
<AvatarImage src='/avatars/01.png' alt='Avatar' />
<AvatarFallback>OM</AvatarFallback>
</Avatar>
<div className='flex flex-1 flex-wrap items-center justify-between'>
<div className='space-y-1'>
<p className='text-sm font-medium leading-none'>Olivia Martin</p>
<p className='text-sm text-muted-foreground'>
olivia.martin@email.com
</p>
</div>
<div className='font-medium'>+$1,999.00</div>
</div>
</div>
<div className='flex items-center gap-4'>
<Avatar className='flex h-9 w-9 items-center justify-center space-y-0 border'>
<AvatarImage src='/avatars/02.png' alt='Avatar' />
<AvatarFallback>JL</AvatarFallback>
</Avatar>
<div className='flex flex-1 flex-wrap items-center justify-between'>
<div className='space-y-1'>
<p className='text-sm font-medium leading-none'>Jackson Lee</p>
<p className='text-sm text-muted-foreground'>
jackson.lee@email.com
</p>
</div>
<div className='font-medium'>+$39.00</div>
</div>
</div>
<div className='flex items-center gap-4'>
<Avatar className='h-9 w-9'>
<AvatarImage src='/avatars/03.png' alt='Avatar' />
<AvatarFallback>IN</AvatarFallback>
</Avatar>
<div className='flex flex-1 flex-wrap items-center justify-between'>
<div className='space-y-1'>
<p className='text-sm font-medium leading-none'>Isabella Nguyen</p>
<p className='text-sm text-muted-foreground'>
isabella.nguyen@email.com
</p>
</div>
<div className='font-medium'>+$299.00</div>
</div>
</div>
<div className='flex items-center gap-4'>
<Avatar className='h-9 w-9'>
<AvatarImage src='/avatars/04.png' alt='Avatar' />
<AvatarFallback>WK</AvatarFallback>
</Avatar>
<div className='flex flex-1 flex-wrap items-center justify-between'>
<div className='space-y-1'>
<p className='text-sm font-medium leading-none'>William Kim</p>
<p className='text-sm text-muted-foreground'>will@email.com</p>
</div>
<div className='font-medium'>+$99.00</div>
</div>
</div>
<div className='flex items-center gap-4'>
<Avatar className='h-9 w-9'>
<AvatarImage src='/avatars/05.png' alt='Avatar' />
<AvatarFallback>SD</AvatarFallback>
</Avatar>
<div className='flex flex-1 flex-wrap items-center justify-between'>
<div className='space-y-1'>
<p className='text-sm font-medium leading-none'>Sofia Davis</p>
<p className='text-sm text-muted-foreground'>
sofia.davis@email.com
</p>
</div>
<div className='font-medium'>+$39.00</div>
</div>
</div>
</div>
)
}
-216
View File
@@ -1,216 +0,0 @@
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Header } from '@/components/layout/header'
import { Main } from '@/components/layout/main'
import { TopNav } from '@/components/layout/top-nav'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { Search } from '@/components/search'
import { ThemeSwitch } from '@/components/theme-switch'
import { Overview } from './components/overview'
import { RecentSales } from './components/recent-sales'
export default function Dashboard() {
return (
<>
{/* ===== Top Heading ===== */}
<Header>
<TopNav links={topNav} />
<div className='ml-auto flex items-center space-x-4'>
<Search />
<ThemeSwitch />
<ProfileDropdown />
</div>
</Header>
{/* ===== Main ===== */}
<Main>
<div className='mb-2 flex items-center justify-between space-y-2'>
<h1 className='text-2xl font-bold tracking-tight'>Dashboard</h1>
<div className='flex items-center space-x-2'>
<Button>Download</Button>
</div>
</div>
<Tabs
orientation='vertical'
defaultValue='overview'
className='space-y-4'
>
<div className='w-full overflow-x-auto pb-2'>
<TabsList>
<TabsTrigger value='overview'>Overview</TabsTrigger>
<TabsTrigger value='analytics' disabled>
Analytics
</TabsTrigger>
<TabsTrigger value='reports' disabled>
Reports
</TabsTrigger>
<TabsTrigger value='notifications' disabled>
Notifications
</TabsTrigger>
</TabsList>
</div>
<TabsContent value='overview' className='space-y-4'>
<div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-4'>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>
Total Revenue
</CardTitle>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
className='h-4 w-4 text-muted-foreground'
>
<path d='M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' />
</svg>
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>$45,231.89</div>
<p className='text-xs text-muted-foreground'>
+20.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>
Subscriptions
</CardTitle>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
className='h-4 w-4 text-muted-foreground'
>
<path d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2' />
<circle cx='9' cy='7' r='4' />
<path d='M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75' />
</svg>
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>+2350</div>
<p className='text-xs text-muted-foreground'>
+180.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>Sales</CardTitle>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
className='h-4 w-4 text-muted-foreground'
>
<rect width='20' height='14' x='2' y='5' rx='2' />
<path d='M2 10h20' />
</svg>
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>+12,234</div>
<p className='text-xs text-muted-foreground'>
+19% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>
Active Now
</CardTitle>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
className='h-4 w-4 text-muted-foreground'
>
<path d='M22 12h-4l-3 9L9 3l-3 9H2' />
</svg>
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>+573</div>
<p className='text-xs text-muted-foreground'>
+201 since last hour
</p>
</CardContent>
</Card>
</div>
<div className='grid grid-cols-1 gap-4 lg:grid-cols-7'>
<Card className='col-span-1 lg:col-span-4'>
<CardHeader>
<CardTitle>Overview</CardTitle>
</CardHeader>
<CardContent className='pl-2'>
<Overview />
</CardContent>
</Card>
<Card className='col-span-1 lg:col-span-3'>
<CardHeader>
<CardTitle>Recent Sales</CardTitle>
<CardDescription>
You made 265 sales this month.
</CardDescription>
</CardHeader>
<CardContent>
<RecentSales />
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</Main>
</>
)
}
const topNav = [
{
title: 'Overview',
href: 'dashboard/overview',
isActive: true,
disabled: false,
},
{
title: 'Customers',
href: 'dashboard/customers',
isActive: false,
disabled: true,
},
{
title: 'Products',
href: 'dashboard/products',
isActive: false,
disabled: true,
},
{
title: 'Settings',
href: 'dashboard/settings',
isActive: false,
disabled: true,
},
]
@@ -1,25 +0,0 @@
import { useNavigate, useRouter } from '@tanstack/react-router'
import { Button } from '@/components/ui/button'
export default function ForbiddenError() {
const navigate = useNavigate()
const { history } = useRouter()
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] font-bold leading-tight'>403</h1>
<span className='font-medium'>Access Forbidden</span>
<p className='text-center text-muted-foreground'>
You don't have necessary permission <br />
to view this resource.
</p>
<div className='mt-6 flex gap-4'>
<Button variant='outline' onClick={() => history.go(-1)}>
Go Back
</Button>
<Button onClick={() => navigate({ to: '/' })}>Back to Home</Button>
</div>
</div>
</div>
)
}
@@ -1,36 +0,0 @@
import { useNavigate, useRouter } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
interface GeneralErrorProps extends React.HTMLAttributes<HTMLDivElement> {
minimal?: boolean
}
export default function GeneralError({
className,
minimal = false,
}: GeneralErrorProps) {
const navigate = useNavigate()
const { history } = useRouter()
return (
<div className={cn('h-svh w-full', className)}>
<div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
{!minimal && (
<h1 className='text-[7rem] font-bold leading-tight'>500</h1>
)}
<span className='font-medium'>Oops! Something went wrong {`:')`}</span>
<p className='text-center text-muted-foreground'>
We apologize for the inconvenience. <br /> Please try again later.
</p>
{!minimal && (
<div className='mt-6 flex gap-4'>
<Button variant='outline' onClick={() => history.go(-1)}>
Go Back
</Button>
<Button onClick={() => navigate({ to: '/' })}>Back to Home</Button>
</div>
)}
</div>
</div>
)
}
@@ -1,19 +0,0 @@
import { Button } from '@/components/ui/button'
export default function MaintenanceError() {
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] font-bold leading-tight'>503</h1>
<span className='font-medium'>Website is under maintenance!</span>
<p className='text-center text-muted-foreground'>
The site is not available at the moment. <br />
We'll be back online shortly.
</p>
<div className='mt-6 flex gap-4'>
<Button variant='outline'>Learn more</Button>
</div>
</div>
</div>
)
}
@@ -1,25 +0,0 @@
import { useNavigate, useRouter } from '@tanstack/react-router'
import { Button } from '@/components/ui/button'
export default function NotFoundError() {
const navigate = useNavigate()
const { history } = useRouter()
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] font-bold leading-tight'>404</h1>
<span className='font-medium'>Oops! Page Not Found!</span>
<p className='text-center text-muted-foreground'>
It seems like the page you're looking for <br />
does not exist or might have been removed.
</p>
<div className='mt-6 flex gap-4'>
<Button variant='outline' onClick={() => history.go(-1)}>
Go Back
</Button>
<Button onClick={() => navigate({ to: '/' })}>Back to Home</Button>
</div>
</div>
</div>
)
}
@@ -1,25 +0,0 @@
import { useNavigate, useRouter } from '@tanstack/react-router'
import { Button } from '@/components/ui/button'
export default function UnauthorisedError() {
const navigate = useNavigate()
const { history } = useRouter()
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] font-bold leading-tight'>401</h1>
<span className='font-medium'>Unauthorized Access</span>
<p className='text-center text-muted-foreground'>
Please log in with the appropriate credentials <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: '/' })}>Back to Home</Button>
</div>
</div>
</div>
)
}
@@ -1,217 +0,0 @@
import { z } from 'zod'
import { format } from 'date-fns'
import { useForm } from 'react-hook-form'
import { CalendarIcon, CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'
import { zodResolver } from '@hookform/resolvers/zod'
import { cn } from '@/lib/utils'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
const languages = [
{ label: 'English', value: 'en' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
{ label: 'Spanish', value: 'es' },
{ label: 'Portuguese', value: 'pt' },
{ label: 'Russian', value: 'ru' },
{ label: 'Japanese', value: 'ja' },
{ label: 'Korean', value: 'ko' },
{ label: 'Chinese', value: 'zh' },
] as const
const accountFormSchema = z.object({
name: z
.string()
.min(2, {
message: 'Name must be at least 2 characters.',
})
.max(30, {
message: 'Name must not be longer than 30 characters.',
}),
dob: z.date({
required_error: 'A date of birth is required.',
}),
language: z.string({
required_error: 'Please select a language.',
}),
})
type AccountFormValues = z.infer<typeof accountFormSchema>
// This can come from your database or API.
const defaultValues: Partial<AccountFormValues> = {
name: '',
}
export function AccountForm() {
const form = useForm<AccountFormValues>({
resolver: zodResolver(accountFormSchema),
defaultValues,
})
function onSubmit(data: AccountFormValues) {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder='Your name' {...field} />
</FormControl>
<FormDescription>
This is the name that will be displayed on your profile and in
emails.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='dob'
render={({ field }) => (
<FormItem className='flex flex-col'>
<FormLabel>Date of birth</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={'outline'}
className={cn(
'w-[240px] pl-3 text-left font-normal',
!field.value && 'text-muted-foreground'
)}
>
{field.value ? (
format(field.value, 'MMM d, yyyy')
) : (
<span>Pick a date</span>
)}
<CalendarIcon className='ml-auto h-4 w-4 opacity-50' />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-auto p-0' align='start'>
<Calendar
mode='single'
selected={field.value}
onSelect={field.onChange}
disabled={(date: Date) =>
date > new Date() || date < new Date('1900-01-01')
}
/>
</PopoverContent>
</Popover>
<FormDescription>
Your date of birth is used to calculate your age.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='language'
render={({ field }) => (
<FormItem className='flex flex-col'>
<FormLabel>Language</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant='outline'
role='combobox'
className={cn(
'w-[200px] justify-between',
!field.value && 'text-muted-foreground'
)}
>
{field.value
? languages.find(
(language) => language.value === field.value
)?.label
: 'Select language'}
<CaretSortIcon className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className='w-[200px] p-0'>
<Command>
<CommandInput placeholder='Search language...' />
<CommandEmpty>No language found.</CommandEmpty>
<CommandGroup>
<CommandList>
{languages.map((language) => (
<CommandItem
value={language.label}
key={language.value}
onSelect={() => {
form.setValue('language', language.value)
}}
>
<CheckIcon
className={cn(
'mr-2 h-4 w-4',
language.value === field.value
? 'opacity-100'
: 'opacity-0'
)}
/>
{language.label}
</CommandItem>
))}
</CommandList>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
This is the language that will be used in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type='submit'>Update account</Button>
</form>
</Form>
)
}
@@ -1,14 +0,0 @@
import ContentSection from '../components/content-section'
import { AccountForm } from './account-form'
export default function SettingsAccount() {
return (
<ContentSection
title='Account'
desc='Update your account settings. Set your preferred language and
timezone.'
>
<AccountForm />
</ContentSection>
)
}
@@ -1,173 +0,0 @@
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { ChevronDownIcon } from '@radix-ui/react-icons'
import { zodResolver } from '@hookform/resolvers/zod'
import { fonts } from '@/config/fonts'
import { cn } from '@/lib/utils'
import { useFont } from '@/context/font-context'
import { useTheme } from '@/context/theme-context'
import { toast } from '@/hooks/use-toast'
import { Button, buttonVariants } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
const appearanceFormSchema = z.object({
theme: z.enum(['light', 'dark'], {
required_error: 'Please select a theme.',
}),
font: z.enum(fonts, {
invalid_type_error: 'Select a font',
required_error: 'Please select a font.',
}),
})
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
export function AppearanceForm() {
const { font, setFont } = useFont()
const { theme, setTheme } = useTheme()
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: theme as 'light' | 'dark',
font,
}
const form = useForm<AppearanceFormValues>({
resolver: zodResolver(appearanceFormSchema),
defaultValues,
})
function onSubmit(data: AppearanceFormValues) {
if (data.font != font) setFont(data.font)
if (data.theme != theme) setTheme(data.theme)
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='font'
render={({ field }) => (
<FormItem>
<FormLabel>Font</FormLabel>
<div className='relative w-max'>
<FormControl>
<select
className={cn(
buttonVariants({ variant: 'outline' }),
'w-[200px] appearance-none font-normal capitalize'
)}
{...field}
>
{fonts.map((font) => (
<option key={font} value={font}>
{font}
</option>
))}
</select>
</FormControl>
<ChevronDownIcon className='absolute right-3 top-2.5 h-4 w-4 opacity-50' />
</div>
<FormDescription className='font-manrope'>
Set the font you want to use in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='theme'
render={({ field }) => (
<FormItem className='space-y-1'>
<FormLabel>Theme</FormLabel>
<FormDescription>
Select the theme for the dashboard.
</FormDescription>
<FormMessage />
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className='grid max-w-md grid-cols-2 gap-8 pt-2'
>
<FormItem>
<FormLabel className='[&:has([data-state=checked])>div]:border-primary'>
<FormControl>
<RadioGroupItem value='light' className='sr-only' />
</FormControl>
<div className='items-center rounded-md border-2 border-muted p-1 hover:border-accent'>
<div className='space-y-2 rounded-sm bg-[#ecedef] p-2'>
<div className='space-y-2 rounded-md bg-white p-2 shadow-sm'>
<div className='h-2 w-[80px] rounded-lg bg-[#ecedef]' />
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-[#ecedef]' />
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-[#ecedef]' />
<div className='h-2 w-[100px] rounded-lg bg-[#ecedef]' />
</div>
</div>
</div>
<span className='block w-full p-2 text-center font-normal'>
Light
</span>
</FormLabel>
</FormItem>
<FormItem>
<FormLabel className='[&:has([data-state=checked])>div]:border-primary'>
<FormControl>
<RadioGroupItem value='dark' className='sr-only' />
</FormControl>
<div className='items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground'>
<div className='space-y-2 rounded-sm bg-slate-950 p-2'>
<div className='space-y-2 rounded-md bg-slate-800 p-2 shadow-sm'>
<div className='h-2 w-[80px] rounded-lg bg-slate-400' />
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-slate-400' />
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
</div>
<div className='flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm'>
<div className='h-4 w-4 rounded-full bg-slate-400' />
<div className='h-2 w-[100px] rounded-lg bg-slate-400' />
</div>
</div>
</div>
<span className='block w-full p-2 text-center font-normal'>
Dark
</span>
</FormLabel>
</FormItem>
</RadioGroup>
</FormItem>
)}
/>
<Button type='submit'>Update preferences</Button>
</form>
</Form>
)
}
@@ -1,14 +0,0 @@
import ContentSection from '../components/content-section'
import { AppearanceForm } from './appearance-form'
export default function SettingsAppearance() {
return (
<ContentSection
title='Appearance'
desc='Customize the appearance of the app. Automatically switch between day
and night themes.'
>
<AppearanceForm />
</ContentSection>
)
}
@@ -1,27 +0,0 @@
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
interface ContentSectionProps {
title: string
desc: string
children: React.JSX.Element
}
export default function ContentSection({
title,
desc,
children,
}: ContentSectionProps) {
return (
<div className='flex flex-1 flex-col'>
<div className='flex-none'>
<h3 className='text-lg font-medium'>{title}</h3>
<p className='text-sm text-muted-foreground'>{desc}</p>
</div>
<Separator className='my-4 flex-none' />
<ScrollArea className='faded-bottom -mx-4 flex-1 scroll-smooth px-4 md:pb-16'>
<div className='-mx-1 px-1.5 lg:max-w-xl'>{children}</div>
</ScrollArea>
</div>
)
}
@@ -1,89 +0,0 @@
import { useState, type JSX } from 'react'
import { useLocation, useNavigate } from '@tanstack/react-router'
import { Link } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: {
href: string
title: string
icon: JSX.Element
}[]
}
export default function SidebarNav({
className,
items,
...props
}: SidebarNavProps) {
const { pathname } = useLocation()
const navigate = useNavigate()
const [val, setVal] = useState(pathname ?? '/settings')
const handleSelect = (e: string) => {
setVal(e)
navigate({ to: e })
}
return (
<>
<div className='p-1 md:hidden'>
<Select value={val} onValueChange={handleSelect}>
<SelectTrigger className='h-12 sm:w-48'>
<SelectValue placeholder='Theme' />
</SelectTrigger>
<SelectContent>
{items.map((item) => (
<SelectItem key={item.href} value={item.href}>
<div className='flex gap-x-4 px-2 py-1'>
<span className='scale-125'>{item.icon}</span>
<span className='text-md'>{item.title}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<ScrollArea
orientation='horizontal'
type='always'
className='hidden w-full min-w-40 bg-background px-1 py-2 md:block'
>
<nav
className={cn(
'flex space-x-2 py-1 lg:flex-col lg:space-x-0 lg:space-y-1',
className
)}
{...props}
>
{items.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
buttonVariants({ variant: 'ghost' }),
pathname === item.href
? 'bg-muted hover:bg-muted'
: 'hover:bg-transparent hover:underline',
'justify-start'
)}
>
<span className='mr-2'>{item.icon}</span>
{item.title}
</Link>
))}
</nav>
</ScrollArea>
</>
)
}
@@ -1,129 +0,0 @@
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
const items = [
{
id: 'recents',
label: 'Recents',
},
{
id: 'home',
label: 'Home',
},
{
id: 'applications',
label: 'Applications',
},
{
id: 'desktop',
label: 'Desktop',
},
{
id: 'downloads',
label: 'Downloads',
},
{
id: 'documents',
label: 'Documents',
},
] as const
const displayFormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: 'You have to select at least one item.',
}),
})
type DisplayFormValues = z.infer<typeof displayFormSchema>
// This can come from your database or API.
const defaultValues: Partial<DisplayFormValues> = {
items: ['recents', 'home'],
}
export function DisplayForm() {
const form = useForm<DisplayFormValues>({
resolver: zodResolver(displayFormSchema),
defaultValues,
})
function onSubmit(data: DisplayFormValues) {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='items'
render={() => (
<FormItem>
<div className='mb-4'>
<FormLabel className='text-base'>Sidebar</FormLabel>
<FormDescription>
Select the items you want to display in the sidebar.
</FormDescription>
</div>
{items.map((item) => (
<FormField
key={item.id}
control={form.control}
name='items'
render={({ field }) => {
return (
<FormItem
key={item.id}
className='flex flex-row items-start space-x-3 space-y-0'
>
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, item.id])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
)
}}
/>
</FormControl>
<FormLabel className='font-normal'>
{item.label}
</FormLabel>
</FormItem>
)
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
<Button type='submit'>Update display</Button>
</form>
</Form>
)
}
@@ -1,13 +0,0 @@
import ContentSection from '../components/content-section'
import { DisplayForm } from './display-form'
export default function SettingsDisplay() {
return (
<ContentSection
title='Display'
desc="Turn items on or off to control what's displayed in the app."
>
<DisplayForm />
</ContentSection>
)
}
-78
View File
@@ -1,78 +0,0 @@
import { Outlet } from '@tanstack/react-router'
import {
IconBrowserCheck,
IconNotification,
IconPalette,
IconTool,
IconUser,
} from '@tabler/icons-react'
import { Separator } from '@/components/ui/separator'
import { Header } from '@/components/layout/header'
import { Main } from '@/components/layout/main'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { Search } from '@/components/search'
import { ThemeSwitch } from '@/components/theme-switch'
import SidebarNav from './components/sidebar-nav'
export default function Settings() {
return (
<>
{/* ===== Top Heading ===== */}
<Header>
<Search />
<div className='ml-auto flex items-center space-x-4'>
<ThemeSwitch />
<ProfileDropdown />
</div>
</Header>
<Main fixed>
<div className='space-y-0.5'>
<h1 className='text-2xl font-bold tracking-tight md:text-3xl'>
Settings
</h1>
<p className='text-muted-foreground'>
Manage your account settings and set e-mail preferences.
</p>
</div>
<Separator className='my-4 lg:my-6' />
<div className='flex flex-1 flex-col space-y-2 overflow-hidden md:space-y-2 lg:flex-row lg:space-x-12 lg:space-y-0'>
<aside className='top-0 lg:sticky lg:w-1/5'>
<SidebarNav items={sidebarNavItems} />
</aside>
<div className='flex w-full overflow-y-hidden p-1 pr-4'>
<Outlet />
</div>
</div>
</Main>
</>
)
}
const sidebarNavItems = [
{
title: 'Profile',
icon: <IconUser size={18} />,
href: '/settings',
},
{
title: 'Account',
icon: <IconTool size={18} />,
href: '/settings/account',
},
{
title: 'Appearance',
icon: <IconPalette size={18} />,
href: '/settings/appearance',
},
{
title: 'Notifications',
icon: <IconNotification size={18} />,
href: '/settings/notifications',
},
{
title: 'Display',
icon: <IconBrowserCheck size={18} />,
href: '/settings/display',
},
]
@@ -1,13 +0,0 @@
import ContentSection from '../components/content-section'
import { NotificationsForm } from './notifications-form'
export default function SettingsNotifications() {
return (
<ContentSection
title='Notifications'
desc='Configure how you receive notifications.'
>
<NotificationsForm />
</ContentSection>
)
}
@@ -1,225 +0,0 @@
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Link } from '@tanstack/react-router'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Switch } from '@/components/ui/switch'
const notificationsFormSchema = z.object({
type: z.enum(['all', 'mentions', 'none'], {
required_error: 'You need to select a notification type.',
}),
mobile: z.boolean().default(false).optional(),
communication_emails: z.boolean().default(false).optional(),
social_emails: z.boolean().default(false).optional(),
marketing_emails: z.boolean().default(false).optional(),
security_emails: z.boolean(),
})
type NotificationsFormValues = z.infer<typeof notificationsFormSchema>
// This can come from your database or API.
const defaultValues: Partial<NotificationsFormValues> = {
communication_emails: false,
marketing_emails: false,
social_emails: true,
security_emails: true,
}
export function NotificationsForm() {
const form = useForm<NotificationsFormValues>({
resolver: zodResolver(notificationsFormSchema),
defaultValues,
})
function onSubmit(data: NotificationsFormValues) {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='type'
render={({ field }) => (
<FormItem className='relative space-y-3'>
<FormLabel>Notify me about...</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className='flex flex-col space-y-1'
>
<FormItem className='flex items-center space-x-3 space-y-0'>
<FormControl>
<RadioGroupItem value='all' />
</FormControl>
<FormLabel className='font-normal'>
All new messages
</FormLabel>
</FormItem>
<FormItem className='flex items-center space-x-3 space-y-0'>
<FormControl>
<RadioGroupItem value='mentions' />
</FormControl>
<FormLabel className='font-normal'>
Direct messages and mentions
</FormLabel>
</FormItem>
<FormItem className='flex items-center space-x-3 space-y-0'>
<FormControl>
<RadioGroupItem value='none' />
</FormControl>
<FormLabel className='font-normal'>Nothing</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className='relative'>
<h3 className='mb-4 text-lg font-medium'>Email Notifications</h3>
<div className='space-y-4'>
<FormField
control={form.control}
name='communication_emails'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>
Communication emails
</FormLabel>
<FormDescription>
Receive emails about your account activity.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='marketing_emails'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>
Marketing emails
</FormLabel>
<FormDescription>
Receive emails about new products, features, and more.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='social_emails'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>Social emails</FormLabel>
<FormDescription>
Receive emails for friend requests, follows, and more.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='security_emails'
render={({ field }) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>Security emails</FormLabel>
<FormDescription>
Receive emails about your account activity and security.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled
aria-readonly
/>
</FormControl>
</FormItem>
)}
/>
</div>
</div>
<FormField
control={form.control}
name='mobile'
render={({ field }) => (
<FormItem className='relative flex flex-row items-start space-x-3 space-y-0'>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className='space-y-1 leading-none'>
<FormLabel>
Use different settings for my mobile devices
</FormLabel>
<FormDescription>
You can manage your mobile notifications in the{' '}
<Link
to='/settings'
className='underline decoration-dashed underline-offset-4 hover:decoration-solid'
>
mobile settings
</Link>{' '}
page.
</FormDescription>
</div>
</FormItem>
)}
/>
<Button type='submit'>Update notifications</Button>
</form>
</Form>
)
}
@@ -1,13 +0,0 @@
import ContentSection from '../components/content-section'
import ProfileForm from './profile-form'
export default function SettingsProfile() {
return (
<ContentSection
title='Profile'
desc='This is how others will see you on the site.'
>
<ProfileForm />
</ContentSection>
)
}
@@ -1,188 +0,0 @@
import { z } from 'zod'
import { useFieldArray, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Link } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
const profileFormSchema = z.object({
username: z
.string()
.min(2, {
message: 'Username must be at least 2 characters.',
})
.max(30, {
message: 'Username must not be longer than 30 characters.',
}),
email: z
.string({
required_error: 'Please select an email to display.',
})
.email(),
bio: z.string().max(160).min(4),
urls: z
.array(
z.object({
value: z.string().url({ message: 'Please enter a valid URL.' }),
})
)
.optional(),
})
type ProfileFormValues = z.infer<typeof profileFormSchema>
// This can come from your database or API.
const defaultValues: Partial<ProfileFormValues> = {
bio: 'I own a computer.',
urls: [
{ value: 'https://shadcn.com' },
{ value: 'http://twitter.com/shadcn' },
],
}
export default function ProfileForm() {
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileFormSchema),
defaultValues,
mode: 'onChange',
})
const { fields, append } = useFieldArray({
name: 'urls',
control: form.control,
})
function onSubmit(data: ProfileFormValues) {
toast({
title: 'You submitted the following values:',
description: (
<pre className='mt-2 w-[340px] rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
<FormField
control={form.control}
name='username'
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder='shadcn' {...field} />
</FormControl>
<FormDescription>
This is your public display name. It can be your real name or a
pseudonym. You can only change this once every 30 days.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder='Select a verified email to display' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='m@example.com'>m@example.com</SelectItem>
<SelectItem value='m@google.com'>m@google.com</SelectItem>
<SelectItem value='m@support.com'>m@support.com</SelectItem>
</SelectContent>
</Select>
<FormDescription>
You can manage verified email addresses in your{' '}
<Link to='/'>email settings</Link>.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='bio'
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder='Tell us a little bit about yourself'
className='resize-none'
{...field}
/>
</FormControl>
<FormDescription>
You can <span>@mention</span> other users and organizations to
link to them.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div>
{fields.map((field, index) => (
<FormField
control={form.control}
key={field.id}
name={`urls.${index}.value`}
render={({ field }) => (
<FormItem>
<FormLabel className={cn(index !== 0 && 'sr-only')}>
URLs
</FormLabel>
<FormDescription className={cn(index !== 0 && 'sr-only')}>
Add links to your website, blog, or social media profiles.
</FormDescription>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
<Button
type='button'
variant='outline'
size='sm'
className='mt-2'
onClick={() => append({ value: '' })}
>
Add URL
</Button>
</div>
<Button type='submit'>Update profile</Button>
</form>
</Form>
)
}
Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff. Show more