[rewrite] Updated frontend structure and played with custom routes
This commit is contained in:
1 parent
43063dcf4e
commit
073f01d070
243 files changed
+11778
-11940
No files matched your search
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"proseWrap": "always",
|
||||
"requireConfig": false,
|
||||
"useTabs": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"bracketSameLine": true
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,175 +1,175 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import {
|
||||
AudioWaveform,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Command,
|
||||
Frame,
|
||||
GalleryVerticalEnd,
|
||||
Map,
|
||||
PieChart,
|
||||
Settings2,
|
||||
SquareTerminal,
|
||||
} from "lucide-react"
|
||||
AudioWaveform,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Command,
|
||||
Frame,
|
||||
GalleryVerticalEnd,
|
||||
Map,
|
||||
PieChart,
|
||||
Settings2,
|
||||
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"
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarRail
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
// This is sample data.
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: "Acme Inc",
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: "Enterprise",
|
||||
},
|
||||
{
|
||||
name: "Acme Corp.",
|
||||
logo: AudioWaveform,
|
||||
plan: "Startup",
|
||||
},
|
||||
{
|
||||
name: "Evil Corp.",
|
||||
logo: Command,
|
||||
plan: "Free",
|
||||
},
|
||||
],
|
||||
navMain: [
|
||||
{
|
||||
title: "Playground",
|
||||
url: "#",
|
||||
icon: SquareTerminal,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "History",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Starred",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Models",
|
||||
url: "#",
|
||||
icon: Bot,
|
||||
items: [
|
||||
{
|
||||
title: "Genesis",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Explorer",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Quantum",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
url: "#",
|
||||
icon: BookOpen,
|
||||
items: [
|
||||
{
|
||||
title: "Introduction",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Get Started",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Tutorials",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Changelog",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: "General",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Billing",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Limits",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
name: "Design Engineering",
|
||||
url: "#",
|
||||
icon: Frame,
|
||||
},
|
||||
{
|
||||
name: "Sales & Marketing",
|
||||
url: "#",
|
||||
icon: PieChart,
|
||||
},
|
||||
{
|
||||
name: "Travel",
|
||||
url: "#",
|
||||
icon: Map,
|
||||
},
|
||||
],
|
||||
}
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg"
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: "Acme Inc",
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: "Enterprise"
|
||||
},
|
||||
{
|
||||
name: "Acme Corp.",
|
||||
logo: AudioWaveform,
|
||||
plan: "Startup"
|
||||
},
|
||||
{
|
||||
name: "Evil Corp.",
|
||||
logo: Command,
|
||||
plan: "Free"
|
||||
}
|
||||
],
|
||||
navMain: [
|
||||
{
|
||||
title: "Playground",
|
||||
url: "#",
|
||||
icon: SquareTerminal,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "History",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Starred",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Models",
|
||||
url: "#",
|
||||
icon: Bot,
|
||||
items: [
|
||||
{
|
||||
title: "Genesis",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Explorer",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Quantum",
|
||||
url: "#"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
url: "#",
|
||||
icon: BookOpen,
|
||||
items: [
|
||||
{
|
||||
title: "Introduction",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Get Started",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Tutorials",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Changelog",
|
||||
url: "#"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: "General",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Team",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Billing",
|
||||
url: "#"
|
||||
},
|
||||
{
|
||||
title: "Limits",
|
||||
url: "#"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
name: "Design Engineering",
|
||||
url: "#",
|
||||
icon: Frame
|
||||
},
|
||||
{
|
||||
name: "Sales & Marketing",
|
||||
url: "#",
|
||||
icon: PieChart
|
||||
},
|
||||
{
|
||||
name: "Travel",
|
||||
url: "#",
|
||||
icon: Map
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavProjects projects={data.projects} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavProjects projects={data.projects} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -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'>
|
||||
<IconPlanet size={72} />
|
||||
<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>
|
||||
)
|
||||
return (
|
||||
<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">
|
||||
This page has not been created yet. <br />
|
||||
Stay tuned though!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +1,94 @@
|
||||
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'
|
||||
IconArrowRightDashed,
|
||||
IconDeviceLaptop,
|
||||
IconMoon,
|
||||
IconSun
|
||||
} from "@tabler/icons-react";
|
||||
import { useSearch } from "@/context/search-context";
|
||||
import { useTheme } from "@/context/theme-context";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command'
|
||||
import { sidebarData } from './layout/data/sidebar-data'
|
||||
import { ScrollArea } from './ui/scroll-area'
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
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]
|
||||
)
|
||||
const runCommand = React.useCallback(
|
||||
(command: () => unknown) => {
|
||||
setOpen(false);
|
||||
command();
|
||||
},
|
||||
[setOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandDialog modal open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder='Type a command or search...' />
|
||||
<CommandList>
|
||||
<ScrollArea type='hover' className='h-72 pr-1'>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{sidebarData.navGroups.map((group) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
{group.items.map((navItem, i) => {
|
||||
if (navItem.url)
|
||||
return (
|
||||
<CommandItem
|
||||
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' />
|
||||
</div>
|
||||
{navItem.title}
|
||||
</CommandItem>
|
||||
)
|
||||
return (
|
||||
<CommandDialog modal open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder="Type a command or search..." />
|
||||
<CommandList>
|
||||
<ScrollArea type="hover" className="h-72 pr-1">
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{sidebarData.navGroups.map((group) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
{group.items.map((navItem, i) => {
|
||||
if (navItem.url)
|
||||
return (
|
||||
<CommandItem
|
||||
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" />
|
||||
</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' />
|
||||
</div>
|
||||
{subItem.title}
|
||||
</CommandItem>
|
||||
))
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading='Theme'>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
|
||||
<IconSun /> <span>Light</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
|
||||
<IconMoon className='scale-90' />
|
||||
<span>Dark</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('system'))}>
|
||||
<IconDeviceLaptop />
|
||||
<span>System</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
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" />
|
||||
</div>
|
||||
{subItem.title}
|
||||
</CommandItem>
|
||||
));
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Theme">
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme("light"))}>
|
||||
<IconSun /> <span>Light</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme("dark"))}>
|
||||
<IconMoon className="scale-90" />
|
||||
<span>Dark</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme("system"))}>
|
||||
<IconDeviceLaptop />
|
||||
<span>System</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +1,66 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
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) {
|
||||
const {
|
||||
title,
|
||||
desc,
|
||||
children,
|
||||
className,
|
||||
confirmText,
|
||||
cancelBtnText,
|
||||
destructive,
|
||||
isLoading,
|
||||
disabled = false,
|
||||
handleConfirm,
|
||||
...actions
|
||||
} = props
|
||||
return (
|
||||
<AlertDialog {...actions}>
|
||||
<AlertDialogContent className={cn(className && className)}>
|
||||
<AlertDialogHeader className='text-left'>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div>{desc}</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{children}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>
|
||||
{cancelBtnText ?? 'Cancel'}
|
||||
</AlertDialogCancel>
|
||||
<Button
|
||||
variant={destructive ? 'destructive' : 'default'}
|
||||
onClick={handleConfirm}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{confirmText ?? 'Continue'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
const {
|
||||
title,
|
||||
desc,
|
||||
children,
|
||||
className,
|
||||
confirmText,
|
||||
cancelBtnText,
|
||||
destructive,
|
||||
isLoading,
|
||||
disabled = false,
|
||||
handleConfirm,
|
||||
...actions
|
||||
} = props;
|
||||
return (
|
||||
<AlertDialog {...actions}>
|
||||
<AlertDialogContent className={cn(className && className)}>
|
||||
<AlertDialogHeader className="text-left">
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div>{desc}</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{children}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>
|
||||
{cancelBtnText ?? "Cancel"}
|
||||
</AlertDialogCancel>
|
||||
<Button
|
||||
variant={destructive ? "destructive" : "default"}
|
||||
onClick={handleConfirm}
|
||||
disabled={disabled || isLoading}>
|
||||
{confirmText ?? "Continue"}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
import {
|
||||
Sidebar,
|
||||
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'
|
||||
Sidebar,
|
||||
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";
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible='icon' variant='floating' {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={sidebarData.teams} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{sidebarData.navGroups.map((props) => (
|
||||
<NavGroup key={props.title} {...props} />
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={sidebarData.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
)
|
||||
return (
|
||||
<Sidebar collapsible="icon" variant="floating" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={sidebarData.teams} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{sidebarData.navGroups.map((props) => (
|
||||
<NavGroup key={props.title} {...props} />
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={sidebarData.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -1,184 +1,189 @@
|
||||
import {
|
||||
IconBarrierBlock,
|
||||
IconBrowserCheck,
|
||||
IconBug,
|
||||
IconChecklist,
|
||||
IconError404,
|
||||
IconHelp,
|
||||
IconLayoutDashboard,
|
||||
IconLock,
|
||||
IconLockAccess,
|
||||
IconMessages,
|
||||
IconNotification,
|
||||
IconPackages,
|
||||
IconPalette,
|
||||
IconServerOff,
|
||||
IconSettings,
|
||||
IconTool,
|
||||
IconUserCog,
|
||||
IconUserOff,
|
||||
IconUsers,
|
||||
} from '@tabler/icons-react'
|
||||
import { AudioWaveform, Command, GalleryVerticalEnd } from 'lucide-react'
|
||||
import { type SidebarData } from '../types'
|
||||
IconBarrierBlock,
|
||||
IconBrowserCheck,
|
||||
IconBug,
|
||||
IconChecklist,
|
||||
IconError404,
|
||||
IconHelp,
|
||||
IconLayoutDashboard,
|
||||
IconLock,
|
||||
IconLockAccess,
|
||||
IconMessages,
|
||||
IconNotification,
|
||||
IconPackages,
|
||||
IconPalette,
|
||||
IconServerOff,
|
||||
IconSettings,
|
||||
IconTool,
|
||||
IconUserCog,
|
||||
IconUserOff,
|
||||
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',
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: 'Shadcn Admin',
|
||||
logo: Command,
|
||||
plan: 'Vite + ShadcnUI',
|
||||
},
|
||||
{
|
||||
name: 'Acme Inc',
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: 'Enterprise',
|
||||
},
|
||||
{
|
||||
name: 'Acme Corp.',
|
||||
logo: AudioWaveform,
|
||||
plan: 'Startup',
|
||||
},
|
||||
],
|
||||
navGroups: [
|
||||
{
|
||||
title: 'General',
|
||||
items: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/',
|
||||
icon: IconLayoutDashboard,
|
||||
},
|
||||
{
|
||||
title: 'Tasks',
|
||||
url: '/tasks',
|
||||
icon: IconChecklist,
|
||||
},
|
||||
{
|
||||
title: 'Apps',
|
||||
url: '/apps',
|
||||
icon: IconPackages,
|
||||
},
|
||||
{
|
||||
title: 'Chats',
|
||||
url: '/chats',
|
||||
badge: '3',
|
||||
icon: IconMessages,
|
||||
},
|
||||
{
|
||||
title: 'Users',
|
||||
url: '/users',
|
||||
icon: IconUsers,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Pages',
|
||||
items: [
|
||||
{
|
||||
title: 'Auth',
|
||||
icon: IconLockAccess,
|
||||
items: [
|
||||
{
|
||||
title: 'Sign In',
|
||||
url: '/sign-in',
|
||||
},
|
||||
{
|
||||
title: 'Sign In (2 Col)',
|
||||
url: '/sign-in-2',
|
||||
},
|
||||
{
|
||||
title: 'Sign Up',
|
||||
url: '/sign-up',
|
||||
},
|
||||
{
|
||||
title: 'Forgot Password',
|
||||
url: '/forgot-password',
|
||||
},
|
||||
{
|
||||
title: 'OTP',
|
||||
url: '/otp',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Errors',
|
||||
icon: IconBug,
|
||||
items: [
|
||||
{
|
||||
title: 'Unauthorized',
|
||||
url: '/401',
|
||||
icon: IconLock,
|
||||
},
|
||||
{
|
||||
title: 'Forbidden',
|
||||
url: '/403',
|
||||
icon: IconUserOff,
|
||||
},
|
||||
{
|
||||
title: 'Not Found',
|
||||
url: '/404',
|
||||
icon: IconError404,
|
||||
},
|
||||
{
|
||||
title: 'Internal Server Error',
|
||||
url: '/500',
|
||||
icon: IconServerOff,
|
||||
},
|
||||
{
|
||||
title: 'Maintenance Error',
|
||||
url: '/503',
|
||||
icon: IconBarrierBlock,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Other',
|
||||
items: [
|
||||
{
|
||||
title: 'Settings',
|
||||
icon: IconSettings,
|
||||
items: [
|
||||
{
|
||||
title: 'Profile',
|
||||
url: '/settings',
|
||||
icon: IconUserCog,
|
||||
},
|
||||
{
|
||||
title: 'Account',
|
||||
url: '/settings/account',
|
||||
icon: IconTool,
|
||||
},
|
||||
{
|
||||
title: 'Appearance',
|
||||
url: '/settings/appearance',
|
||||
icon: IconPalette,
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
url: '/settings/notifications',
|
||||
icon: IconNotification,
|
||||
},
|
||||
{
|
||||
title: 'Display',
|
||||
url: '/settings/display',
|
||||
icon: IconBrowserCheck,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help Center',
|
||||
url: '/help-center',
|
||||
icon: IconHelp,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
user: {
|
||||
name: "satnaing",
|
||||
email: "satnaingdev@gmail.com",
|
||||
avatar: "/avatars/shadcn.jpg"
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: "Shadcn Admin",
|
||||
logo: Command,
|
||||
plan: "Vite + ShadcnUI"
|
||||
},
|
||||
{
|
||||
name: "Acme Inc",
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: "Enterprise"
|
||||
},
|
||||
{
|
||||
name: "Acme Corp.",
|
||||
logo: AudioWaveform,
|
||||
plan: "Startup"
|
||||
}
|
||||
],
|
||||
navGroups: [
|
||||
{
|
||||
title: "General",
|
||||
items: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/",
|
||||
icon: IconLayoutDashboard
|
||||
},
|
||||
{
|
||||
title: "Tasks",
|
||||
url: "/tasks",
|
||||
icon: IconChecklist
|
||||
},
|
||||
{
|
||||
title: "Apps",
|
||||
url: "/apps",
|
||||
icon: IconPackages
|
||||
},
|
||||
{
|
||||
title: "Chats",
|
||||
url: "/chats",
|
||||
badge: "3",
|
||||
icon: IconMessages
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
url: "/users",
|
||||
icon: IconUsers
|
||||
},
|
||||
{
|
||||
title: "NBA",
|
||||
url: "/nba",
|
||||
icon: IconChecklist
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Pages",
|
||||
items: [
|
||||
{
|
||||
title: "Auth",
|
||||
icon: IconLockAccess,
|
||||
items: [
|
||||
{
|
||||
title: "Sign In",
|
||||
url: "/sign-in"
|
||||
},
|
||||
{
|
||||
title: "Sign In (2 Col)",
|
||||
url: "/sign-in-2"
|
||||
},
|
||||
{
|
||||
title: "Sign Up",
|
||||
url: "/sign-up"
|
||||
},
|
||||
{
|
||||
title: "Forgot Password",
|
||||
url: "/forgot-password"
|
||||
},
|
||||
{
|
||||
title: "OTP",
|
||||
url: "/otp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Errors",
|
||||
icon: IconBug,
|
||||
items: [
|
||||
{
|
||||
title: "Unauthorized",
|
||||
url: "/401",
|
||||
icon: IconLock
|
||||
},
|
||||
{
|
||||
title: "Forbidden",
|
||||
url: "/403",
|
||||
icon: IconUserOff
|
||||
},
|
||||
{
|
||||
title: "Not Found",
|
||||
url: "/404",
|
||||
icon: IconError404
|
||||
},
|
||||
{
|
||||
title: "Internal Server Error",
|
||||
url: "/500",
|
||||
icon: IconServerOff
|
||||
},
|
||||
{
|
||||
title: "Maintenance Error",
|
||||
url: "/503",
|
||||
icon: IconBarrierBlock
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Other",
|
||||
items: [
|
||||
{
|
||||
title: "Settings",
|
||||
icon: IconSettings,
|
||||
items: [
|
||||
{
|
||||
title: "Profile",
|
||||
url: "/settings",
|
||||
icon: IconUserCog
|
||||
},
|
||||
{
|
||||
title: "Account",
|
||||
url: "/settings/account",
|
||||
icon: IconTool
|
||||
},
|
||||
{
|
||||
title: "Appearance",
|
||||
url: "/settings/appearance",
|
||||
icon: IconPalette
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
url: "/settings/notifications",
|
||||
icon: IconNotification
|
||||
},
|
||||
{
|
||||
title: "Display",
|
||||
url: "/settings/display",
|
||||
icon: IconBrowserCheck
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Help Center",
|
||||
url: "/help-center",
|
||||
icon: IconHelp
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -1,48 +1,47 @@
|
||||
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 = ({
|
||||
className,
|
||||
fixed,
|
||||
children,
|
||||
...props
|
||||
className,
|
||||
fixed,
|
||||
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)
|
||||
}
|
||||
React.useEffect(() => {
|
||||
const onScroll = () => {
|
||||
setOffset(document.body.scrollTop || document.documentElement.scrollTop);
|
||||
};
|
||||
|
||||
// Add scroll listener to the body
|
||||
document.addEventListener('scroll', onScroll, { passive: true })
|
||||
// Add scroll listener to the body
|
||||
document.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
// Clean up the event listener on unmount
|
||||
return () => document.removeEventListener('scroll', onScroll)
|
||||
}, [])
|
||||
// Clean up the event listener on unmount
|
||||
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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
|
||||
<Separator orientation='vertical' className='h-6' />
|
||||
{children}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<SidebarTrigger variant="outline" className="scale-125 sm:scale-100" />
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
{children}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
Header.displayName = 'Header'
|
||||
Header.displayName = "Header";
|
||||
@@ -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'
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<main
|
||||
className={cn(
|
||||
"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";
|
||||
@@ -1,180 +1,175 @@
|
||||
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'
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { Badge } from '../ui/badge'
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
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'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
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 })
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{title}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => {
|
||||
const key = `${item.title}-${item.url}`
|
||||
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}`;
|
||||
|
||||
if (!item.items)
|
||||
return <SidebarMenuLink key={key} item={item} href={href} />
|
||||
if (!item.items)
|
||||
return <SidebarMenuLink key={key} item={item} href={href} />;
|
||||
|
||||
if (state === 'collapsed')
|
||||
return (
|
||||
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
|
||||
)
|
||||
if (state === "collapsed")
|
||||
return (
|
||||
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
|
||||
);
|
||||
|
||||
return <SidebarMenuCollapsible key={key} item={item} href={href} />
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
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()
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, item)}
|
||||
tooltip={item.title}
|
||||
>
|
||||
<Link to={item.url} onClick={() => setOpenMobile(false)}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
const { setOpenMobile } = useSidebar();
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, item)}
|
||||
tooltip={item.title}>
|
||||
<Link to={item.url} onClick={() => setOpenMobile(false)}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarMenuCollapsible = ({
|
||||
item,
|
||||
href,
|
||||
item,
|
||||
href
|
||||
}: {
|
||||
item: NavCollapsible
|
||||
href: string
|
||||
item: NavCollapsible;
|
||||
href: string;
|
||||
}) => {
|
||||
const { setOpenMobile } = useSidebar()
|
||||
return (
|
||||
<Collapsible
|
||||
asChild
|
||||
defaultOpen={checkIsActive(href, item, true)}
|
||||
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' />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className='CollapsibleContent'>
|
||||
<SidebarMenuSub>
|
||||
{item.items.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, subItem)}
|
||||
>
|
||||
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
||||
{subItem.icon && <subItem.icon />}
|
||||
<span>{subItem.title}</span>
|
||||
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
const { setOpenMobile } = useSidebar();
|
||||
return (
|
||||
<Collapsible
|
||||
asChild
|
||||
defaultOpen={checkIsActive(href, item, true)}
|
||||
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" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="CollapsibleContent">
|
||||
<SidebarMenuSub>
|
||||
{item.items.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={checkIsActive(href, subItem)}>
|
||||
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
||||
{subItem.icon && <subItem.icon />}
|
||||
<span>{subItem.title}</span>
|
||||
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarMenuCollapsedDropdown = ({
|
||||
item,
|
||||
href,
|
||||
item,
|
||||
href
|
||||
}: {
|
||||
item: NavCollapsible
|
||||
href: string
|
||||
item: NavCollapsible;
|
||||
href: string;
|
||||
}) => {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
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' />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side='right' align='start' sideOffset={4}>
|
||||
<DropdownMenuLabel>
|
||||
{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' : ''}`}
|
||||
>
|
||||
{sub.icon && <sub.icon />}
|
||||
<span className='max-w-52 text-wrap'>{sub.title}</span>
|
||||
{sub.badge && (
|
||||
<span className='ml-auto text-xs'>{sub.badge}</span>
|
||||
)}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
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" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="start" sideOffset={4}>
|
||||
<DropdownMenuLabel>
|
||||
{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" : ""}`}>
|
||||
{sub.icon && <sub.icon />}
|
||||
<span className="max-w-52 text-wrap">{sub.title}</span>
|
||||
{sub.badge && (
|
||||
<span className="ml-auto text-xs">{sub.badge}</span>
|
||||
)}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
function checkIsActive(href: string, item: NavItem, mainNav = false) {
|
||||
return (
|
||||
href === item.url || // /endpint?search=param
|
||||
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])
|
||||
)
|
||||
return (
|
||||
href === item.url || // /endpint?search=param
|
||||
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])
|
||||
);
|
||||
}
|
||||
@@ -1,114 +1,112 @@
|
||||
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'
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles
|
||||
} from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
user
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<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'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<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>
|
||||
<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'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<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>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings/account'>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings/notifications'>
|
||||
<Bell />
|
||||
Notifications
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<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">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<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>
|
||||
<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">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<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>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings/account">
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings">
|
||||
<CreditCard />
|
||||
Billing
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings/notifications">
|
||||
<Bell />
|
||||
Notifications
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +1,83 @@
|
||||
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,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function TeamSwitcher({
|
||||
teams,
|
||||
teams
|
||||
}: {
|
||||
teams: {
|
||||
name: string
|
||||
logo: React.ElementType
|
||||
plan: string
|
||||
}[]
|
||||
teams: {
|
||||
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>
|
||||
<SidebarMenuItem>
|
||||
<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' />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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'>
|
||||
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' />
|
||||
</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' />
|
||||
</div>
|
||||
<div className='font-medium text-muted-foreground'>Add team</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
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">
|
||||
{activeTeam.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||
</div>
|
||||
<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">
|
||||
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" />
|
||||
</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" />
|
||||
</div>
|
||||
<div className="font-medium text-muted-foreground">Add team</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +1,64 @@
|
||||
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'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
links: {
|
||||
title: string
|
||||
href: string
|
||||
isActive: boolean
|
||||
disabled?: boolean
|
||||
}[]
|
||||
links: {
|
||||
title: string;
|
||||
href: string;
|
||||
isActive: boolean;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function TopNav({ className, links, ...props }: TopNavProps) {
|
||||
return (
|
||||
<>
|
||||
<div className='md:hidden'>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size='icon' variant='outline'>
|
||||
<IconMenu />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<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}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className="md:hidden">
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" variant="outline">
|
||||
<IconMenu />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<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}>
|
||||
{title}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
className={cn(
|
||||
'hidden items-center space-x-4 md:flex lg:space-x-6',
|
||||
className
|
||||
)}
|
||||
{...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'}`}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
<nav
|
||||
className={cn(
|
||||
"hidden items-center space-x-4 md:flex lg:space-x-6",
|
||||
className
|
||||
)}
|
||||
{...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"}`}>
|
||||
{title}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -1,85 +1,85 @@
|
||||
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'
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
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 = '',
|
||||
children,
|
||||
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
|
||||
}
|
||||
useEffect(() => {
|
||||
if (checkOverflow(ref.current)) {
|
||||
setIsOverflown(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOverflown(false)
|
||||
}, [])
|
||||
setIsOverflown(false);
|
||||
}, []);
|
||||
|
||||
if (!isOverflown)
|
||||
return (
|
||||
<div ref={ref} className={cn('truncate', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
if (!isOverflown)
|
||||
return (
|
||||
<div ref={ref} className={cn("truncate", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='hidden sm:block'>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div ref={ref} className={cn('truncate', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className={contentClassName}>{children}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className='sm:hidden'>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div ref={ref} className={cn('truncate', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={cn('w-fit', contentClassName)}>
|
||||
<p>{children}</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<div className="hidden sm:block">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div ref={ref} className={cn("truncate", className)}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className={contentClassName}>{children}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="sm:hidden">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div ref={ref} className={cn("truncate", className)}>
|
||||
{children}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={cn("w-fit", contentClassName)}>
|
||||
<p>{children}</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const checkOverflow = (textContainer: HTMLDivElement | null) => {
|
||||
if (textContainer) {
|
||||
return (
|
||||
textContainer.offsetHeight < textContainer.scrollHeight ||
|
||||
textContainer.offsetWidth < textContainer.scrollWidth
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (textContainer) {
|
||||
return (
|
||||
textContainer.offsetHeight < textContainer.scrollHeight ||
|
||||
textContainer.offsetWidth < textContainer.scrollWidth
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -1,73 +1,72 @@
|
||||
"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"
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
items
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon?: LucideIcon
|
||||
isActive?: boolean
|
||||
items?: {
|
||||
title: string
|
||||
url: string
|
||||
}[]
|
||||
}[]
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: LucideIcon;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
}[];
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<a href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
className="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<a href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,89 +1,88 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Folder,
|
||||
Forward,
|
||||
MoreHorizontal,
|
||||
Trash2,
|
||||
type LucideIcon,
|
||||
} from "lucide-react"
|
||||
Folder,
|
||||
Forward,
|
||||
MoreHorizontal,
|
||||
Trash2,
|
||||
type LucideIcon
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavProjects({
|
||||
projects,
|
||||
projects
|
||||
}: {
|
||||
projects: {
|
||||
name: string
|
||||
url: string
|
||||
icon: LucideIcon
|
||||
}[]
|
||||
projects: {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: LucideIcon;
|
||||
}[];
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{projects.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction showOnHover>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-48 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<Folder className="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Forward className="text-muted-foreground" />
|
||||
<span>Share Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<MoreHorizontal className="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{projects.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction showOnHover>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-48 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}>
|
||||
<DropdownMenuItem>
|
||||
<Folder className="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Forward className="text-muted-foreground" />
|
||||
<span>Share Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<MoreHorizontal className="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +1,108 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
} from "lucide-react"
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles
|
||||
} from "lucide-react";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/ui/avatar"
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
user
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<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">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</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>
|
||||
<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">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</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>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<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">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</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>
|
||||
<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">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</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>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
>
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
"type"
|
||||
>;
|
||||
|
||||
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
({ className, disabled, ...props }, ref) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false)
|
||||
return (
|
||||
<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'
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
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)}
|
||||
>
|
||||
{showPassword ? <IconEye size={18} /> : <IconEyeOff size={18} />}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
PasswordInput.displayName = 'PasswordInput'
|
||||
({ className, disabled, ...props }, ref) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
return (
|
||||
<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"
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
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)}>
|
||||
{showPassword ? <IconEye size={18} /> : <IconEyeOff size={18} />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
PasswordInput.displayName = "PasswordInput";
|
||||
|
||||
export { PasswordInput }
|
||||
export { PasswordInput };
|
||||
@@ -1,454 +1,454 @@
|
||||
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>[]
|
||||
/**
|
||||
* className for the input container
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* `aria-label` for the input fields
|
||||
*/
|
||||
ariaLabel?: string
|
||||
/**
|
||||
* If set, the pin input receives focus on mount, `false` by default
|
||||
*/
|
||||
autoFocus?: boolean
|
||||
/**
|
||||
* Called when value changes
|
||||
*/
|
||||
onChange?: (value: string) => void
|
||||
/**
|
||||
* Called when all inputs have valid value
|
||||
*/
|
||||
onComplete?: (value: string) => void
|
||||
/**
|
||||
* Called when any input doesn't have value
|
||||
*/
|
||||
onIncomplete?: (value: string) => void
|
||||
/**
|
||||
* `name` attribute for input fields
|
||||
*/
|
||||
name?: string
|
||||
/**
|
||||
* `form` attribute for hidden input
|
||||
*/
|
||||
form?: string
|
||||
/**
|
||||
* If set, the input's value will be masked just like password input. This field is `false` by default
|
||||
*/
|
||||
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
|
||||
/**
|
||||
* Uncontrolled pin input default value.
|
||||
*/
|
||||
defaultValue?: string
|
||||
/**
|
||||
* Controlled pin input value.
|
||||
*/
|
||||
value?: string
|
||||
/**
|
||||
* The type of value pin input should allow, `alphanumeric` by default
|
||||
*/
|
||||
type?: 'numeric' | 'alphanumeric'
|
||||
/**
|
||||
* Placeholder for input fields, `○` by default
|
||||
*/
|
||||
placeholder?: string
|
||||
/**
|
||||
* If set, the user cannot set the value, `false` by default
|
||||
*/
|
||||
readOnly?: boolean
|
||||
/**
|
||||
* If set, the input fields are disabled, `false` by default
|
||||
*/
|
||||
disabled?: boolean
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
children:
|
||||
| React.ReactElement<typeof PinInputField>
|
||||
| React.ReactElement<typeof PinInputField>[];
|
||||
/**
|
||||
* className for the input container
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* `aria-label` for the input fields
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
/**
|
||||
* If set, the pin input receives focus on mount, `false` by default
|
||||
*/
|
||||
autoFocus?: boolean;
|
||||
/**
|
||||
* Called when value changes
|
||||
*/
|
||||
onChange?: (value: string) => void;
|
||||
/**
|
||||
* Called when all inputs have valid value
|
||||
*/
|
||||
onComplete?: (value: string) => void;
|
||||
/**
|
||||
* Called when any input doesn't have value
|
||||
*/
|
||||
onIncomplete?: (value: string) => void;
|
||||
/**
|
||||
* `name` attribute for input fields
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* `form` attribute for hidden input
|
||||
*/
|
||||
form?: string;
|
||||
/**
|
||||
* If set, the input's value will be masked just like password input. This field is `false` by default
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* Uncontrolled pin input default value.
|
||||
*/
|
||||
defaultValue?: string;
|
||||
/**
|
||||
* Controlled pin input value.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* The type of value pin input should allow, `alphanumeric` by default
|
||||
*/
|
||||
type?: "numeric" | "alphanumeric";
|
||||
/**
|
||||
* Placeholder for input fields, `○` by default
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* If set, the user cannot set the value, `false` by default
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* If set, the input fields are disabled, `false` by default
|
||||
*/
|
||||
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 {
|
||||
defaultValue,
|
||||
value,
|
||||
onChange,
|
||||
onComplete,
|
||||
onIncomplete,
|
||||
placeholder = '○',
|
||||
type = 'alphanumeric',
|
||||
name,
|
||||
form,
|
||||
otp = false,
|
||||
mask = false,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
autoFocus = false,
|
||||
ariaLabel = '',
|
||||
...rest
|
||||
} = props
|
||||
const {
|
||||
defaultValue,
|
||||
value,
|
||||
onChange,
|
||||
onComplete,
|
||||
onIncomplete,
|
||||
placeholder = "○",
|
||||
type = "alphanumeric",
|
||||
name,
|
||||
form,
|
||||
otp = false,
|
||||
mask = false,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
autoFocus = false,
|
||||
ariaLabel = "",
|
||||
...rest
|
||||
} = 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({
|
||||
value,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
type,
|
||||
length,
|
||||
readOnly,
|
||||
})
|
||||
// pins, pinValue, refMap, ...handlers
|
||||
const { pins, pinValue, refMap, ...handlers } = usePinInput({
|
||||
value,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
type,
|
||||
length,
|
||||
readOnly
|
||||
});
|
||||
|
||||
/* call onChange func if pinValue changes */
|
||||
React.useEffect(() => {
|
||||
if (!onChange) return
|
||||
onChange(pinValue)
|
||||
}, [onChange, pinValue])
|
||||
/* call onChange func if pinValue changes */
|
||||
React.useEffect(() => {
|
||||
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)
|
||||
React.useEffect(() => {
|
||||
if (pinValue.length === length && completeRef.current === false) {
|
||||
completeRef.current = true
|
||||
if (onComplete) onComplete(pinValue)
|
||||
}
|
||||
if (pinValue.length !== length && completeRef.current === true) {
|
||||
completeRef.current = false
|
||||
if (onIncomplete) onIncomplete(pinValue)
|
||||
}
|
||||
}, [length, onComplete, onIncomplete, pinValue, pins, value])
|
||||
/* call onComplete/onIncomplete func if pinValue is valid and completed/incompleted */
|
||||
const completeRef = React.useRef(pinValue.length === length);
|
||||
React.useEffect(() => {
|
||||
if (pinValue.length === length && completeRef.current === false) {
|
||||
completeRef.current = true;
|
||||
if (onComplete) onComplete(pinValue);
|
||||
}
|
||||
if (pinValue.length !== length && completeRef.current === true) {
|
||||
completeRef.current = false;
|
||||
if (onIncomplete) onIncomplete(pinValue);
|
||||
}
|
||||
}, [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 (node) {
|
||||
node.focus()
|
||||
}
|
||||
}, [autoFocus, refMap])
|
||||
/* focus on first input field if autoFocus is set */
|
||||
React.useEffect(() => {
|
||||
if (!autoFocus) return;
|
||||
const node = refMap?.get(0);
|
||||
if (node) {
|
||||
node.focus();
|
||||
}
|
||||
}, [autoFocus, refMap]);
|
||||
|
||||
const skipRef = React.useRef(0)
|
||||
let counter = 0
|
||||
const clones = validChildren.map((child) => {
|
||||
if (child.type === PinInputField) {
|
||||
const pinIndex = counter
|
||||
counter = counter + 1
|
||||
return React.cloneElement(child, {
|
||||
name,
|
||||
inputKey: `input-${pinIndex}`,
|
||||
value: length > pinIndex ? pins[pinIndex] : '',
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
handlers.handleChange(e, pinIndex),
|
||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) =>
|
||||
handlers.handleFocus(e, pinIndex),
|
||||
onBlur: () => handlers.handleBlur(pinIndex),
|
||||
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||
handlers.handleKeyDown(e, pinIndex),
|
||||
onPaste: (e: React.ClipboardEvent<HTMLInputElement>) =>
|
||||
handlers.handlePaste(e),
|
||||
placeholder: placeholder,
|
||||
type: type,
|
||||
mask: mask,
|
||||
autoComplete: otp ? 'one-time-code' : 'off',
|
||||
disabled: disabled,
|
||||
readOnly: readOnly,
|
||||
'aria-label': ariaLabel
|
||||
? ariaLabel
|
||||
: `Pin input ${counter} of ${length}`,
|
||||
ref: (node: HTMLInputElement | null) => {
|
||||
if (node) {
|
||||
refMap?.set(pinIndex, node)
|
||||
} else {
|
||||
refMap?.delete(pinIndex)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
skipRef.current = skipRef.current + 1
|
||||
return child
|
||||
})
|
||||
const skipRef = React.useRef(0);
|
||||
let counter = 0;
|
||||
const clones = validChildren.map((child) => {
|
||||
if (child.type === PinInputField) {
|
||||
const pinIndex = counter;
|
||||
counter = counter + 1;
|
||||
return React.cloneElement(child, {
|
||||
name,
|
||||
inputKey: `input-${pinIndex}`,
|
||||
value: length > pinIndex ? pins[pinIndex] : "",
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
handlers.handleChange(e, pinIndex),
|
||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) =>
|
||||
handlers.handleFocus(e, pinIndex),
|
||||
onBlur: () => handlers.handleBlur(pinIndex),
|
||||
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||
handlers.handleKeyDown(e, pinIndex),
|
||||
onPaste: (e: React.ClipboardEvent<HTMLInputElement>) =>
|
||||
handlers.handlePaste(e),
|
||||
placeholder: placeholder,
|
||||
type: type,
|
||||
mask: mask,
|
||||
autoComplete: otp ? "one-time-code" : "off",
|
||||
disabled: disabled,
|
||||
readOnly: readOnly,
|
||||
"aria-label": ariaLabel
|
||||
? ariaLabel
|
||||
: `Pin input ${counter} of ${length}`,
|
||||
ref: (node: HTMLInputElement | null) => {
|
||||
if (node) {
|
||||
refMap?.set(pinIndex, node);
|
||||
} else {
|
||||
refMap?.delete(pinIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
skipRef.current = skipRef.current + 1;
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<PinInputContext.Provider value={true}>
|
||||
<div ref={ref} aria-label='Pin Input' className={className} {...rest}>
|
||||
{clones}
|
||||
<input type='hidden' name={name} form={form} value={pinValue} />
|
||||
</div>
|
||||
</PinInputContext.Provider>
|
||||
)
|
||||
}
|
||||
PinInput.displayName = 'PinInput'
|
||||
return (
|
||||
<PinInputContext.Provider value={true}>
|
||||
<div ref={ref} aria-label="Pin Input" className={className} {...rest}>
|
||||
{clones}
|
||||
<input type="hidden" name={name} form={form} value={pinValue} />
|
||||
</div>
|
||||
</PinInputContext.Provider>
|
||||
);
|
||||
};
|
||||
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'>,
|
||||
keyof _PinInputFieldProps
|
||||
> {
|
||||
component?: T
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<"input">,
|
||||
keyof _PinInputFieldProps
|
||||
> {
|
||||
component?: T;
|
||||
}
|
||||
|
||||
const PinInputField = <T extends React.ElementType = 'input'>({
|
||||
className,
|
||||
component,
|
||||
...props
|
||||
const PinInputField = <T extends React.ElementType = "input">({
|
||||
className,
|
||||
component,
|
||||
...props
|
||||
}: PinInputFieldProps<T> &
|
||||
(React.ComponentType<T> extends undefined
|
||||
? never
|
||||
: React.ComponentProps<T>)) => {
|
||||
const { mask, type, inputKey, ...rest } = props as _PinInputFieldProps &
|
||||
React.ComponentProps<T>
|
||||
(React.ComponentType<T> extends undefined
|
||||
? never
|
||||
: React.ComponentProps<T>)) => {
|
||||
const { mask, type, inputKey, ...rest } = props as _PinInputFieldProps &
|
||||
React.ComponentProps<T>;
|
||||
|
||||
// Check if PinInputField is used within PinInput
|
||||
const isInsidePinInput = React.useContext(PinInputContext)
|
||||
if (!isInsidePinInput) {
|
||||
throw new Error(
|
||||
`PinInputField must be used within ${PinInput.displayName}.`
|
||||
)
|
||||
}
|
||||
// Check if PinInputField is used within PinInput
|
||||
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)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Element
|
||||
key={inputKey}
|
||||
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 = ({
|
||||
value,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
type,
|
||||
length,
|
||||
readOnly,
|
||||
value,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
type,
|
||||
length,
|
||||
readOnly
|
||||
}: UsePinInputProps) => {
|
||||
const pinInputs = React.useMemo(
|
||||
() =>
|
||||
Array.from({ length }, (_, index) =>
|
||||
defaultValue
|
||||
? defaultValue.charAt(index)
|
||||
: value
|
||||
? value.charAt(index)
|
||||
: ''
|
||||
),
|
||||
[defaultValue, length, value]
|
||||
)
|
||||
const pinInputs = React.useMemo(
|
||||
() =>
|
||||
Array.from({ length }, (_, index) =>
|
||||
defaultValue
|
||||
? 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])
|
||||
/**
|
||||
* Update pins when values change programmatically.
|
||||
* This syncs the pins if the `defaultValue` or `value` prop is updated.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
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()
|
||||
}
|
||||
return itemsRef.current
|
||||
}
|
||||
function getMap() {
|
||||
if (!itemsRef.current) {
|
||||
// Initialize the Map on first usage.
|
||||
itemsRef.current = new Map();
|
||||
}
|
||||
return itemsRef.current;
|
||||
}
|
||||
|
||||
function getNode(index: number) {
|
||||
const map = getMap()
|
||||
const node = map?.get(index)
|
||||
return node
|
||||
}
|
||||
function getNode(index: number) {
|
||||
const map = getMap();
|
||||
const node = map?.get(index);
|
||||
return node;
|
||||
}
|
||||
|
||||
function focusInput(itemId: number) {
|
||||
const node = getNode(itemId)
|
||||
if (node) {
|
||||
node.focus()
|
||||
node.placeholder = ''
|
||||
}
|
||||
}
|
||||
function focusInput(itemId: number) {
|
||||
const node = getNode(itemId);
|
||||
if (node) {
|
||||
node.focus();
|
||||
node.placeholder = "";
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(
|
||||
event: React.FocusEvent<HTMLInputElement>,
|
||||
index: number
|
||||
) {
|
||||
event.target.select()
|
||||
focusInput(index)
|
||||
}
|
||||
function handleFocus(
|
||||
event: React.FocusEvent<HTMLInputElement>,
|
||||
index: number
|
||||
) {
|
||||
event.target.select();
|
||||
focusInput(index);
|
||||
}
|
||||
|
||||
function handleBlur(index: number) {
|
||||
const node = getNode(index)
|
||||
if (node) {
|
||||
node.placeholder = placeholder
|
||||
}
|
||||
}
|
||||
function handleBlur(index: number) {
|
||||
const node = getNode(index);
|
||||
if (node) {
|
||||
node.placeholder = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
function updateInputField(val: string, index: number) {
|
||||
const node = getNode(index)
|
||||
function updateInputField(val: string, index: number) {
|
||||
const node = getNode(index);
|
||||
|
||||
if (node) {
|
||||
node.value = val
|
||||
}
|
||||
if (node) {
|
||||
node.value = val;
|
||||
}
|
||||
|
||||
setPins((prev) =>
|
||||
prev.map((p, i) => {
|
||||
if (i === index) {
|
||||
return val
|
||||
} else {
|
||||
return p
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
setPins((prev) =>
|
||||
prev.map((p, i) => {
|
||||
if (i === index) {
|
||||
return val;
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
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 pastedVal = React.useRef<null | string>(null)
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>, index: number) {
|
||||
const inputValue = e.target.value
|
||||
const pastedValue = pastedVal.current
|
||||
const inputChar =
|
||||
pastedValue && pastedValue.length === length
|
||||
? pastedValue.charAt(length - 1)
|
||||
: inputValue.slice(-1)
|
||||
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 inputChar =
|
||||
pastedValue && pastedValue.length === length
|
||||
? pastedValue.charAt(length - 1)
|
||||
: inputValue.slice(-1);
|
||||
|
||||
if (validate(inputChar)) {
|
||||
updateInputField(inputChar, index)
|
||||
pastedVal.current = null
|
||||
if (inputValue.length > 0) {
|
||||
focusInput(index + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (validate(inputChar)) {
|
||||
updateInputField(inputChar, index);
|
||||
pastedVal.current = null;
|
||||
if (inputValue.length > 0) {
|
||||
focusInput(index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
const copyValue = event.clipboardData
|
||||
.getData('text/plain')
|
||||
.replace(/[\n\r\s]+/g, '')
|
||||
const copyArr = copyValue.split('').slice(0, length)
|
||||
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
|
||||
event.preventDefault();
|
||||
const copyValue = event.clipboardData
|
||||
.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)
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < copyArr.length) {
|
||||
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
|
||||
function handleKeyDown(
|
||||
event: React.KeyboardEvent<HTMLInputElement>,
|
||||
index: number
|
||||
) {
|
||||
const { ctrlKey, key, shiftKey, metaKey } = event;
|
||||
|
||||
if (type === 'numeric') {
|
||||
const canTypeSign =
|
||||
key === 'Backspace' ||
|
||||
key === 'Tab' ||
|
||||
key === 'Control' ||
|
||||
key === 'Delete' ||
|
||||
(ctrlKey && key === 'v') ||
|
||||
(metaKey && key === 'v')
|
||||
? true
|
||||
: !Number.isNaN(Number(key))
|
||||
if (type === "numeric") {
|
||||
const canTypeSign =
|
||||
key === "Backspace" ||
|
||||
key === "Tab" ||
|
||||
key === "Control" ||
|
||||
key === "Delete" ||
|
||||
(ctrlKey && key === "v") ||
|
||||
(metaKey && key === "v")
|
||||
? true
|
||||
: !Number.isNaN(Number(key));
|
||||
|
||||
if (!canTypeSign || readOnly) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
if (!canTypeSign || readOnly) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pins,
|
||||
pinValue,
|
||||
refMap: getMap(),
|
||||
handleFocus,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
handlePaste,
|
||||
handleKeyDown,
|
||||
}
|
||||
}
|
||||
return {
|
||||
pins,
|
||||
pinValue,
|
||||
refMap: getMap(),
|
||||
handleFocus,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
handlePaste,
|
||||
handleKeyDown
|
||||
};
|
||||
};
|
||||
|
||||
/* ========== Util Func ========== */
|
||||
|
||||
const getValidChildren = (children: React.ReactNode) =>
|
||||
React.Children.toArray(children).filter((child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
return React.isValidElement(child)
|
||||
}
|
||||
throw new Error(`${PinInput.displayName} contains invalid children.`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as React.ReactElement<any>[]
|
||||
React.Children.toArray(children).filter((child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
return React.isValidElement(child);
|
||||
}
|
||||
throw new Error(`${PinInput.displayName} contains invalid children.`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-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)
|
||||
}
|
||||
}).length
|
||||
React.Children.toArray(children).filter((child) => {
|
||||
if (React.isValidElement(child) && child.type === PinInputField) {
|
||||
return React.isValidElement(child);
|
||||
}
|
||||
}).length;
|
||||
|
||||
export { PinInput, PinInputField }
|
||||
export { PinInput, PinInputField };
|
||||
@@ -1,65 +1,65 @@
|
||||
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,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
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' />
|
||||
<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'>
|
||||
satnaingdev@gmail.com
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Profile
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Billing
|
||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Settings
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
Log out
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
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" />
|
||||
<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">
|
||||
satnaingdev@gmail.com
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings">
|
||||
Profile
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings">
|
||||
Billing
|
||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings">
|
||||
Settings
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
Log out
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -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()
|
||||
return (
|
||||
<Button
|
||||
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',
|
||||
className
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<IconSearch
|
||||
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
|
||||
</kbd>
|
||||
</Button>
|
||||
)
|
||||
export function Search({ className = "", placeholder = "Search" }: Props) {
|
||||
const { setOpen } = useSearch();
|
||||
return (
|
||||
<Button
|
||||
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",
|
||||
className
|
||||
)}
|
||||
onClick={() => setOpen(true)}>
|
||||
<IconSearch
|
||||
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
|
||||
</kbd>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +1,62 @@
|
||||
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'
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
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({
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
isPending,
|
||||
items,
|
||||
placeholder,
|
||||
disabled,
|
||||
className = '',
|
||||
isControlled = false,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
isPending,
|
||||
items,
|
||||
placeholder,
|
||||
disabled,
|
||||
className = "",
|
||||
isControlled = false
|
||||
}: SelectDropdownProps) {
|
||||
const defaultState = isControlled
|
||||
? { value: defaultValue, onValueChange }
|
||||
: { defaultValue, onValueChange }
|
||||
return (
|
||||
<Select {...defaultState}>
|
||||
<FormControl>
|
||||
<SelectTrigger disabled={disabled} className={cn(className)}>
|
||||
<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' />
|
||||
{' '}
|
||||
Loading...
|
||||
</div>
|
||||
</SelectItem>
|
||||
) : (
|
||||
items?.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
const defaultState = isControlled
|
||||
? { value: defaultValue, onValueChange }
|
||||
: { defaultValue, onValueChange };
|
||||
return (
|
||||
<Select {...defaultState}>
|
||||
<FormControl>
|
||||
<SelectTrigger disabled={disabled} className={cn(className)}>
|
||||
<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" />
|
||||
{" "}
|
||||
Loading...
|
||||
</div>
|
||||
</SelectItem>
|
||||
) : (
|
||||
items?.map(({ label, value }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
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'
|
||||
>
|
||||
Skip to Main
|
||||
</a>
|
||||
)
|
||||
}
|
||||
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">
|
||||
Skip to Main
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkipToMain
|
||||
export default SkipToMain;
|
||||
@@ -1,89 +1,86 @@
|
||||
"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,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function TeamSwitcher({
|
||||
teams,
|
||||
teams
|
||||
}: {
|
||||
teams: {
|
||||
name: string
|
||||
logo: React.ElementType
|
||||
plan: string
|
||||
}[]
|
||||
teams: {
|
||||
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>
|
||||
<SidebarMenuItem>
|
||||
<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" />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
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" />
|
||||
</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" />
|
||||
</div>
|
||||
<div className="font-medium text-muted-foreground">Add team</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
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">
|
||||
{activeTeam.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||
</div>
|
||||
<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">
|
||||
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" />
|
||||
</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" />
|
||||
</div>
|
||||
<div className="font-medium text-muted-foreground">Add team</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
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])
|
||||
/* 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]);
|
||||
|
||||
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>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
Light{' '}
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn('ml-auto', theme !== 'light' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
Dark
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn('ml-auto', theme !== 'dark' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
System
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn('ml-auto', theme !== 'system' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
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>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light{" "}
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn("ml-auto", theme !== "light" && "hidden")}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn("ml-auto", theme !== "dark" && "hidden")}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
<IconCheck
|
||||
size={14}
|
||||
className={cn("ml-auto", theme !== "system" && "hidden")}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +1,138 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ 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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
));
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = 'AlertDialogHeader'
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader";
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = 'AlertDialogFooter'
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter";
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'mt-2 sm:mt-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel
|
||||
};
|
||||
@@ -1,58 +1,58 @@
|
||||
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',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
"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",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role='alert'
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = 'Alert'
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = 'AlertTitle'
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = 'AlertDescription'
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
@@ -1,47 +1,47 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn('aspect-square h-full w-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center rounded-full bg-muted',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
@@ -1,35 +1,35 @@
|
||||
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',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
||||
secondary:
|
||||
'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',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
"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",
|
||||
secondary:
|
||||
"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"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
@@ -1,115 +1,113 @@
|
||||
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
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
||||
Breadcrumb.displayName = "Breadcrumb"
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav"> & {
|
||||
separator?: React.ReactNode;
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
||||
Breadcrumb.displayName = "Breadcrumb";
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbList.displayName = "BreadcrumbList"
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbList.displayName = "BreadcrumbList";
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean
|
||||
}
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
|
||||
{...props}>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis
|
||||
};
|
||||
@@ -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',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'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',
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
"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",
|
||||
destructive:
|
||||
"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",
|
||||
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"
|
||||
},
|
||||
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"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
@@ -1,69 +1,69 @@
|
||||
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,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
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',
|
||||
nav_button: cn(
|
||||
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',
|
||||
head_cell:
|
||||
'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'
|
||||
),
|
||||
day: cn(
|
||||
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_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',
|
||||
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_range_middle:
|
||||
'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' />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
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",
|
||||
nav_button: cn(
|
||||
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",
|
||||
head_cell:
|
||||
"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"
|
||||
),
|
||||
day: cn(
|
||||
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_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",
|
||||
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_range_middle:
|
||||
"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" />
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Calendar.displayName = 'Calendar'
|
||||
Calendar.displayName = "Calendar";
|
||||
|
||||
export { Calendar }
|
||||
export { Calendar };
|
||||
@@ -1,75 +1,82 @@
|
||||
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,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-xl border bg-card text-card-foreground shadow',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = 'Card'
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
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,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = 'CardFooter'
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent
|
||||
};
|
||||
@@ -1,27 +1,25 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn('flex items-center justify-center text-current')}
|
||||
>
|
||||
<Check className='h-4 w-4' />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
@@ -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 };
|
||||
@@ -1,162 +1,162 @@
|
||||
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'
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<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'>
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<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">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
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' />
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className='py-6 text-center text-sm'
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = 'CommandShortcut'
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator
|
||||
};
|
||||
@@ -1,121 +1,120 @@
|
||||
'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>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...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>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...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>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
};
|
||||
@@ -1,198 +1,195 @@
|
||||
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
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className='ml-auto' />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...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' />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...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" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...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' />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...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" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup
|
||||
};
|
||||
+131
-131
@@ -1,175 +1,175 @@
|
||||
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'
|
||||
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";
|
||||
|
||||
const Form = FormProvider
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
{} as FormFieldContextValue
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<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>')
|
||||
}
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
{} as FormItemContextValue
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
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} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = 'FormItem'
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
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)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = 'FormLabel'
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = 'FormControl'
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
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)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = 'FormDescription'
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
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
|
||||
}
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn('text-[0.8rem] font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField
|
||||
};
|
||||
@@ -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'>>(
|
||||
({ 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',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = '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",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
@@ -1,23 +1,23 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
@@ -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) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
@@ -1,41 +1,40 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn('grid gap-2', className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...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
|
||||
return (
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...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;
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
@@ -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'
|
||||
extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
|
||||
orientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
ScrollAreaProps
|
||||
>(({ className, children, orientation = 'vertical', ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
className={cn(
|
||||
'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
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
ScrollAreaProps
|
||||
>(({ className, children, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
className={cn(
|
||||
"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;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ 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]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className='relative flex-1 rounded-full bg-border' />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ 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]",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -1,156 +1,150 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className='h-4 w-4 opacity-50' />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className='h-4 w-4' />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className='h-4 w-4' />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...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) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...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)]'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ 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",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...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)]"
|
||||
)}>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...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' />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...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" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton
|
||||
};
|
||||
@@ -1,28 +1,28 @@
|
||||
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>
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = 'horizontal', decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
@@ -1,139 +1,138 @@
|
||||
'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>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ 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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
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',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
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',
|
||||
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',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
}
|
||||
)
|
||||
"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",
|
||||
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",
|
||||
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"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ 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>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ 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>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = 'SheetFooter'
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,15 +1,15 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn('animate-pulse rounded-md bg-primary/10', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
@@ -1,26 +1,25 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ 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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
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'
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
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"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch }
|
||||
export { Switch };
|
||||
@@ -1,119 +1,119 @@
|
||||
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>
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className='relative w-full overflow-auto'>
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = 'Table'
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
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,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = 'TableBody'
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = 'TableFooter'
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = 'TableRow'
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = 'TableHead'
|
||||
<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]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = 'TableCell'
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = 'TableCaption'
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption
|
||||
};
|
||||
@@ -1,52 +1,52 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
@@ -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'>
|
||||
HTMLTextAreaElement,
|
||||
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',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = 'Textarea'
|
||||
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",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea }
|
||||
export { Textarea };
|
||||
@@ -1,126 +1,125 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
<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]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
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',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border bg-background text-foreground',
|
||||
destructive:
|
||||
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
"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",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
toast-close=''
|
||||
{...props}
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
<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",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm opacity-90', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
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,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction
|
||||
};
|
||||
@@ -1,33 +1,33 @@
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from '@/components/ui/toast'
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
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'>
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
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>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
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',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -25,4 +25,4 @@
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const fonts = ['inter', 'manrope', 'system'] as const
|
||||
export const fonts = ["inter", "manrope", "system"] as const;
|
||||
@@ -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 [font, _setFont] = useState<Font>(() => {
|
||||
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
|
||||
root.classList.forEach((cls) => {
|
||||
if (cls.startsWith('font-')) root.classList.remove(cls)
|
||||
})
|
||||
root.classList.add(`font-${font}`)
|
||||
}
|
||||
useEffect(() => {
|
||||
const applyFont = (font: string) => {
|
||||
const root = document.documentElement;
|
||||
root.classList.forEach((cls) => {
|
||||
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)
|
||||
}
|
||||
const setFont = (font: 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)
|
||||
if (!context) {
|
||||
throw new Error('useFont must be used within a FontProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
const context = useContext(FontContext);
|
||||
if (!context) {
|
||||
throw new Error("useFont must be used within a FontProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', down)
|
||||
return () => document.removeEventListener('keydown', down)
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setOpen((open) => !open);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SearchContext.Provider value={{ open, setOpen }}>
|
||||
{children}
|
||||
<CommandMenu />
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
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>')
|
||||
}
|
||||
if (!searchContext) {
|
||||
throw new Error("useSearch has to be used within <SearchContext.Provider>");
|
||||
}
|
||||
|
||||
return searchContext
|
||||
}
|
||||
return searchContext;
|
||||
};
|
||||
@@ -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',
|
||||
...props
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, _setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
)
|
||||
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)')
|
||||
useEffect(() => {
|
||||
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
|
||||
}
|
||||
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
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
if (theme === 'system') {
|
||||
applyTheme('system')
|
||||
}
|
||||
}
|
||||
const handleChange = () => {
|
||||
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)
|
||||
}
|
||||
const setTheme = (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
_setTheme(theme);
|
||||
};
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme,
|
||||
}
|
||||
const value = {
|
||||
theme,
|
||||
setTheme
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
)
|
||||
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')
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -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.',
|
||||
},
|
||||
]
|
||||
@@ -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'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>
|
||||
)
|
||||
}
|
||||
@@ -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'>
|
||||
“This template has saved me countless hours of work and
|
||||
helped me deliver stunning designs to my clients faster than ever
|
||||
before.”
|
||||
</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]
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { labels, priorities, statuses } from '../data/data'
|
||||
import { Task } from '../data/schema'
|
||||
import { DataTableColumnHeader } from './data-table-column-header'
|
||||
import { DataTableRowActions } from './data-table-row-actions'
|
||||
|
||||
export const columns: ColumnDef<Task>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label='Select all'
|
||||
className='translate-y-[2px]'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label='Select row'
|
||||
className='translate-y-[2px]'
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title='Task' />
|
||||
),
|
||||
cell: ({ row }) => <div className='w-[80px]'>{row.getValue('id')}</div>,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'title',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title='Title' />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const label = labels.find((label) => label.value === row.original.label)
|
||||
|
||||
return (
|
||||
<div className='flex space-x-2'>
|
||||
{label && <Badge variant='outline'>{label.label}</Badge>}
|
||||
<span className='max-w-32 truncate font-medium sm:max-w-72 md:max-w-[31rem]'>
|
||||
{row.getValue('title')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title='Status' />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const status = statuses.find(
|
||||
(status) => status.value === row.getValue('status')
|
||||
)
|
||||
|
||||
if (!status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex w-[100px] items-center'>
|
||||
{status.icon && (
|
||||
<status.icon className='mr-2 h-4 w-4 text-muted-foreground' />
|
||||
)}
|
||||
<span>{status.label}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'priority',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title='Priority' />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const priority = priorities.find(
|
||||
(priority) => priority.value === row.getValue('priority')
|
||||
)
|
||||
|
||||
if (!priority) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
{priority.icon && (
|
||||
<priority.icon className='mr-2 h-4 w-4 text-muted-foreground' />
|
||||
)}
|
||||
<span>{priority.label}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => <DataTableRowActions row={row} />,
|
||||
},
|
||||
]
|
||||
Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user