[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
Vendored
+5
-1
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
// Backend settings
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/__pycache__/**": true
|
"**/__pycache__/**": true
|
||||||
},
|
},
|
||||||
"mypy-type-checker.args": ["--config-file='backend/mypy.ini'"],
|
"mypy-type-checker.args": ["--config-file='backend/mypy.ini'"],
|
||||||
"python.defaultInterpreterPath": "./backend/.venv/bin/python"
|
"python.defaultInterpreterPath": "./backend/.venv/bin/python",
|
||||||
|
|
||||||
|
// Frontend settings
|
||||||
|
"prettier.configPath": "./frontend/.prettierrc"
|
||||||
}
|
}
|
||||||
@@ -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 {
|
import {
|
||||||
AudioWaveform,
|
AudioWaveform,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Bot,
|
Bot,
|
||||||
Command,
|
Command,
|
||||||
Frame,
|
Frame,
|
||||||
GalleryVerticalEnd,
|
GalleryVerticalEnd,
|
||||||
Map,
|
Map,
|
||||||
PieChart,
|
PieChart,
|
||||||
Settings2,
|
Settings2,
|
||||||
SquareTerminal,
|
SquareTerminal
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
|
||||||
import { NavMain } from "@/components/nav-main"
|
import { NavMain } from "@/components/nav-main";
|
||||||
import { NavProjects } from "@/components/nav-projects"
|
import { NavProjects } from "@/components/nav-projects";
|
||||||
import { NavUser } from "@/components/nav-user"
|
import { NavUser } from "@/components/nav-user";
|
||||||
import { TeamSwitcher } from "@/components/team-switcher"
|
import { TeamSwitcher } from "@/components/team-switcher";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarRail,
|
SidebarRail
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
// This is sample data.
|
// This is sample data.
|
||||||
const data = {
|
const data = {
|
||||||
user: {
|
user: {
|
||||||
name: "shadcn",
|
name: "shadcn",
|
||||||
email: "m@example.com",
|
email: "m@example.com",
|
||||||
avatar: "/avatars/shadcn.jpg",
|
avatar: "/avatars/shadcn.jpg"
|
||||||
},
|
},
|
||||||
teams: [
|
teams: [
|
||||||
{
|
{
|
||||||
name: "Acme Inc",
|
name: "Acme Inc",
|
||||||
logo: GalleryVerticalEnd,
|
logo: GalleryVerticalEnd,
|
||||||
plan: "Enterprise",
|
plan: "Enterprise"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Acme Corp.",
|
name: "Acme Corp.",
|
||||||
logo: AudioWaveform,
|
logo: AudioWaveform,
|
||||||
plan: "Startup",
|
plan: "Startup"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Evil Corp.",
|
name: "Evil Corp.",
|
||||||
logo: Command,
|
logo: Command,
|
||||||
plan: "Free",
|
plan: "Free"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "Playground",
|
title: "Playground",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: SquareTerminal,
|
icon: SquareTerminal,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "History",
|
title: "History",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Starred",
|
title: "Starred",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Models",
|
title: "Models",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: Bot,
|
icon: Bot,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Genesis",
|
title: "Genesis",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Explorer",
|
title: "Explorer",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Quantum",
|
title: "Quantum",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Documentation",
|
title: "Documentation",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Introduction",
|
title: "Introduction",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Get Started",
|
title: "Get Started",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Tutorials",
|
title: "Tutorials",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Changelog",
|
title: "Changelog",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: Settings2,
|
icon: Settings2,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "General",
|
title: "General",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Team",
|
title: "Team",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Billing",
|
title: "Billing",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Limits",
|
title: "Limits",
|
||||||
url: "#",
|
url: "#"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "Design Engineering",
|
name: "Design Engineering",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: Frame,
|
icon: Frame
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Sales & Marketing",
|
name: "Sales & Marketing",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: PieChart,
|
icon: PieChart
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Travel",
|
name: "Travel",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: Map,
|
icon: Map
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
}
|
};
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<TeamSwitcher teams={data.teams} />
|
<TeamSwitcher teams={data.teams} />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain items={data.navMain} />
|
<NavMain items={data.navMain} />
|
||||||
<NavProjects projects={data.projects} />
|
<NavProjects projects={data.projects} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser user={data.user} />
|
<NavUser user={data.user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { IconPlanet } from '@tabler/icons-react'
|
import { IconPlanet } from "@tabler/icons-react";
|
||||||
|
|
||||||
export default function ComingSoon() {
|
export default function ComingSoon() {
|
||||||
return (
|
return (
|
||||||
<div className='h-svh'>
|
<div className="h-svh">
|
||||||
<div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
|
<div className="m-auto flex h-full w-full flex-col items-center justify-center gap-2">
|
||||||
<IconPlanet size={72} />
|
<IconPlanet size={72} />
|
||||||
<h1 className='text-4xl font-bold leading-tight'>Coming Soon 👀</h1>
|
<h1 className="text-4xl font-bold leading-tight">Coming Soon 👀</h1>
|
||||||
<p className='text-center text-muted-foreground'>
|
<p className="text-center text-muted-foreground">
|
||||||
This page has not been created yet. <br />
|
This page has not been created yet. <br />
|
||||||
Stay tuned though!
|
Stay tuned though!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,96 +1,94 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import {
|
import {
|
||||||
IconArrowRightDashed,
|
IconArrowRightDashed,
|
||||||
IconDeviceLaptop,
|
IconDeviceLaptop,
|
||||||
IconMoon,
|
IconMoon,
|
||||||
IconSun,
|
IconSun
|
||||||
} from '@tabler/icons-react'
|
} from "@tabler/icons-react";
|
||||||
import { useSearch } from '@/context/search-context'
|
import { useSearch } from "@/context/search-context";
|
||||||
import { useTheme } from '@/context/theme-context'
|
import { useTheme } from "@/context/theme-context";
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator
|
||||||
} from '@/components/ui/command'
|
} from "@/components/ui/command";
|
||||||
import { sidebarData } from './layout/data/sidebar-data'
|
import { sidebarData } from "./layout/data/sidebar-data";
|
||||||
import { ScrollArea } from './ui/scroll-area'
|
import { ScrollArea } from "./ui/scroll-area";
|
||||||
|
|
||||||
export function CommandMenu() {
|
export function CommandMenu() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme();
|
||||||
const { open, setOpen } = useSearch()
|
const { open, setOpen } = useSearch();
|
||||||
|
|
||||||
const runCommand = React.useCallback(
|
const runCommand = React.useCallback(
|
||||||
(command: () => unknown) => {
|
(command: () => unknown) => {
|
||||||
setOpen(false)
|
setOpen(false);
|
||||||
command()
|
command();
|
||||||
},
|
},
|
||||||
[setOpen]
|
[setOpen]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog modal open={open} onOpenChange={setOpen}>
|
<CommandDialog modal open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput placeholder='Type a command or search...' />
|
<CommandInput placeholder="Type a command or search..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<ScrollArea type='hover' className='h-72 pr-1'>
|
<ScrollArea type="hover" className="h-72 pr-1">
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
{sidebarData.navGroups.map((group) => (
|
{sidebarData.navGroups.map((group) => (
|
||||||
<CommandGroup key={group.title} heading={group.title}>
|
<CommandGroup key={group.title} heading={group.title}>
|
||||||
{group.items.map((navItem, i) => {
|
{group.items.map((navItem, i) => {
|
||||||
if (navItem.url)
|
if (navItem.url)
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={`${navItem.url}-${i}`}
|
key={`${navItem.url}-${i}`}
|
||||||
value={navItem.title}
|
value={navItem.title}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
runCommand(() => navigate({ to: navItem.url }))
|
runCommand(() => navigate({ to: navItem.url }));
|
||||||
}}
|
}}>
|
||||||
>
|
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
||||||
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
|
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
|
||||||
<IconArrowRightDashed className='size-2 text-muted-foreground/80' />
|
</div>
|
||||||
</div>
|
{navItem.title}
|
||||||
{navItem.title}
|
</CommandItem>
|
||||||
</CommandItem>
|
);
|
||||||
)
|
|
||||||
|
|
||||||
return navItem.items?.map((subItem, i) => (
|
return navItem.items?.map((subItem, i) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={`${subItem.url}-${i}`}
|
key={`${subItem.url}-${i}`}
|
||||||
value={subItem.title}
|
value={subItem.title}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
runCommand(() => navigate({ to: subItem.url }))
|
runCommand(() => navigate({ to: subItem.url }));
|
||||||
}}
|
}}>
|
||||||
>
|
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
||||||
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
|
<IconArrowRightDashed className="size-2 text-muted-foreground/80" />
|
||||||
<IconArrowRightDashed className='size-2 text-muted-foreground/80' />
|
</div>
|
||||||
</div>
|
{subItem.title}
|
||||||
{subItem.title}
|
</CommandItem>
|
||||||
</CommandItem>
|
));
|
||||||
))
|
})}
|
||||||
})}
|
</CommandGroup>
|
||||||
</CommandGroup>
|
))}
|
||||||
))}
|
<CommandSeparator />
|
||||||
<CommandSeparator />
|
<CommandGroup heading="Theme">
|
||||||
<CommandGroup heading='Theme'>
|
<CommandItem onSelect={() => runCommand(() => setTheme("light"))}>
|
||||||
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
|
<IconSun /> <span>Light</span>
|
||||||
<IconSun /> <span>Light</span>
|
</CommandItem>
|
||||||
</CommandItem>
|
<CommandItem onSelect={() => runCommand(() => setTheme("dark"))}>
|
||||||
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
|
<IconMoon className="scale-90" />
|
||||||
<IconMoon className='scale-90' />
|
<span>Dark</span>
|
||||||
<span>Dark</span>
|
</CommandItem>
|
||||||
</CommandItem>
|
<CommandItem onSelect={() => runCommand(() => setTheme("system"))}>
|
||||||
<CommandItem onSelect={() => runCommand(() => setTheme('system'))}>
|
<IconDeviceLaptop />
|
||||||
<IconDeviceLaptop />
|
<span>System</span>
|
||||||
<span>System</span>
|
</CommandItem>
|
||||||
</CommandItem>
|
</CommandGroup>
|
||||||
</CommandGroup>
|
</ScrollArea>
|
||||||
</ScrollArea>
|
</CommandList>
|
||||||
</CommandList>
|
</CommandDialog>
|
||||||
</CommandDialog>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,67 +1,66 @@
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
open: boolean
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void;
|
||||||
title: React.ReactNode
|
title: React.ReactNode;
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
desc: React.JSX.Element | string
|
desc: React.JSX.Element | string;
|
||||||
cancelBtnText?: string
|
cancelBtnText?: string;
|
||||||
confirmText?: React.ReactNode
|
confirmText?: React.ReactNode;
|
||||||
destructive?: boolean
|
destructive?: boolean;
|
||||||
handleConfirm: () => void
|
handleConfirm: () => void;
|
||||||
isLoading?: boolean
|
isLoading?: boolean;
|
||||||
className?: string
|
className?: string;
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfirmDialog(props: ConfirmDialogProps) {
|
export function ConfirmDialog(props: ConfirmDialogProps) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
desc,
|
desc,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
confirmText,
|
confirmText,
|
||||||
cancelBtnText,
|
cancelBtnText,
|
||||||
destructive,
|
destructive,
|
||||||
isLoading,
|
isLoading,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
handleConfirm,
|
handleConfirm,
|
||||||
...actions
|
...actions
|
||||||
} = props
|
} = props;
|
||||||
return (
|
return (
|
||||||
<AlertDialog {...actions}>
|
<AlertDialog {...actions}>
|
||||||
<AlertDialogContent className={cn(className && className)}>
|
<AlertDialogContent className={cn(className && className)}>
|
||||||
<AlertDialogHeader className='text-left'>
|
<AlertDialogHeader className="text-left">
|
||||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
<AlertDialogDescription asChild>
|
<AlertDialogDescription asChild>
|
||||||
<div>{desc}</div>
|
<div>{desc}</div>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
{children}
|
{children}
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel disabled={isLoading}>
|
<AlertDialogCancel disabled={isLoading}>
|
||||||
{cancelBtnText ?? 'Cancel'}
|
{cancelBtnText ?? "Cancel"}
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
<Button
|
<Button
|
||||||
variant={destructive ? 'destructive' : 'default'}
|
variant={destructive ? "destructive" : "default"}
|
||||||
onClick={handleConfirm}
|
onClick={handleConfirm}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}>
|
||||||
>
|
{confirmText ?? "Continue"}
|
||||||
{confirmText ?? 'Continue'}
|
</Button>
|
||||||
</Button>
|
</AlertDialogFooter>
|
||||||
</AlertDialogFooter>
|
</AlertDialogContent>
|
||||||
</AlertDialogContent>
|
</AlertDialog>
|
||||||
</AlertDialog>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarRail,
|
SidebarRail
|
||||||
} from '@/components/ui/sidebar'
|
} from "@/components/ui/sidebar";
|
||||||
import { NavGroup } from '@/components/layout/nav-group'
|
import { NavGroup } from "@/components/layout/nav-group";
|
||||||
import { NavUser } from '@/components/layout/nav-user'
|
import { NavUser } from "@/components/layout/nav-user";
|
||||||
import { TeamSwitcher } from '@/components/layout/team-switcher'
|
import { TeamSwitcher } from "@/components/layout/team-switcher";
|
||||||
import { sidebarData } from './data/sidebar-data'
|
import { sidebarData } from "./data/sidebar-data";
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible='icon' variant='floating' {...props}>
|
<Sidebar collapsible="icon" variant="floating" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<TeamSwitcher teams={sidebarData.teams} />
|
<TeamSwitcher teams={sidebarData.teams} />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
{sidebarData.navGroups.map((props) => (
|
{sidebarData.navGroups.map((props) => (
|
||||||
<NavGroup key={props.title} {...props} />
|
<NavGroup key={props.title} {...props} />
|
||||||
))}
|
))}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser user={sidebarData.user} />
|
<NavUser user={sidebarData.user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,184 +1,189 @@
|
|||||||
import {
|
import {
|
||||||
IconBarrierBlock,
|
IconBarrierBlock,
|
||||||
IconBrowserCheck,
|
IconBrowserCheck,
|
||||||
IconBug,
|
IconBug,
|
||||||
IconChecklist,
|
IconChecklist,
|
||||||
IconError404,
|
IconError404,
|
||||||
IconHelp,
|
IconHelp,
|
||||||
IconLayoutDashboard,
|
IconLayoutDashboard,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconLockAccess,
|
IconLockAccess,
|
||||||
IconMessages,
|
IconMessages,
|
||||||
IconNotification,
|
IconNotification,
|
||||||
IconPackages,
|
IconPackages,
|
||||||
IconPalette,
|
IconPalette,
|
||||||
IconServerOff,
|
IconServerOff,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconTool,
|
IconTool,
|
||||||
IconUserCog,
|
IconUserCog,
|
||||||
IconUserOff,
|
IconUserOff,
|
||||||
IconUsers,
|
IconUsers
|
||||||
} from '@tabler/icons-react'
|
} from "@tabler/icons-react";
|
||||||
import { AudioWaveform, Command, GalleryVerticalEnd } from 'lucide-react'
|
import { AudioWaveform, Command, GalleryVerticalEnd } from "lucide-react";
|
||||||
import { type SidebarData } from '../types'
|
import { type SidebarData } from "../types";
|
||||||
|
|
||||||
export const sidebarData: SidebarData = {
|
export const sidebarData: SidebarData = {
|
||||||
user: {
|
user: {
|
||||||
name: 'satnaing',
|
name: "satnaing",
|
||||||
email: 'satnaingdev@gmail.com',
|
email: "satnaingdev@gmail.com",
|
||||||
avatar: '/avatars/shadcn.jpg',
|
avatar: "/avatars/shadcn.jpg"
|
||||||
},
|
},
|
||||||
teams: [
|
teams: [
|
||||||
{
|
{
|
||||||
name: 'Shadcn Admin',
|
name: "Shadcn Admin",
|
||||||
logo: Command,
|
logo: Command,
|
||||||
plan: 'Vite + ShadcnUI',
|
plan: "Vite + ShadcnUI"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Acme Inc',
|
name: "Acme Inc",
|
||||||
logo: GalleryVerticalEnd,
|
logo: GalleryVerticalEnd,
|
||||||
plan: 'Enterprise',
|
plan: "Enterprise"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Acme Corp.',
|
name: "Acme Corp.",
|
||||||
logo: AudioWaveform,
|
logo: AudioWaveform,
|
||||||
plan: 'Startup',
|
plan: "Startup"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
navGroups: [
|
navGroups: [
|
||||||
{
|
{
|
||||||
title: 'General',
|
title: "General",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: "Dashboard",
|
||||||
url: '/',
|
url: "/",
|
||||||
icon: IconLayoutDashboard,
|
icon: IconLayoutDashboard
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tasks',
|
title: "Tasks",
|
||||||
url: '/tasks',
|
url: "/tasks",
|
||||||
icon: IconChecklist,
|
icon: IconChecklist
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Apps',
|
title: "Apps",
|
||||||
url: '/apps',
|
url: "/apps",
|
||||||
icon: IconPackages,
|
icon: IconPackages
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Chats',
|
title: "Chats",
|
||||||
url: '/chats',
|
url: "/chats",
|
||||||
badge: '3',
|
badge: "3",
|
||||||
icon: IconMessages,
|
icon: IconMessages
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Users',
|
title: "Users",
|
||||||
url: '/users',
|
url: "/users",
|
||||||
icon: IconUsers,
|
icon: IconUsers
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
},
|
title: "NBA",
|
||||||
{
|
url: "/nba",
|
||||||
title: 'Pages',
|
icon: IconChecklist
|
||||||
items: [
|
}
|
||||||
{
|
]
|
||||||
title: 'Auth',
|
},
|
||||||
icon: IconLockAccess,
|
{
|
||||||
items: [
|
title: "Pages",
|
||||||
{
|
items: [
|
||||||
title: 'Sign In',
|
{
|
||||||
url: '/sign-in',
|
title: "Auth",
|
||||||
},
|
icon: IconLockAccess,
|
||||||
{
|
items: [
|
||||||
title: 'Sign In (2 Col)',
|
{
|
||||||
url: '/sign-in-2',
|
title: "Sign In",
|
||||||
},
|
url: "/sign-in"
|
||||||
{
|
},
|
||||||
title: 'Sign Up',
|
{
|
||||||
url: '/sign-up',
|
title: "Sign In (2 Col)",
|
||||||
},
|
url: "/sign-in-2"
|
||||||
{
|
},
|
||||||
title: 'Forgot Password',
|
{
|
||||||
url: '/forgot-password',
|
title: "Sign Up",
|
||||||
},
|
url: "/sign-up"
|
||||||
{
|
},
|
||||||
title: 'OTP',
|
{
|
||||||
url: '/otp',
|
title: "Forgot Password",
|
||||||
},
|
url: "/forgot-password"
|
||||||
],
|
},
|
||||||
},
|
{
|
||||||
{
|
title: "OTP",
|
||||||
title: 'Errors',
|
url: "/otp"
|
||||||
icon: IconBug,
|
}
|
||||||
items: [
|
]
|
||||||
{
|
},
|
||||||
title: 'Unauthorized',
|
{
|
||||||
url: '/401',
|
title: "Errors",
|
||||||
icon: IconLock,
|
icon: IconBug,
|
||||||
},
|
items: [
|
||||||
{
|
{
|
||||||
title: 'Forbidden',
|
title: "Unauthorized",
|
||||||
url: '/403',
|
url: "/401",
|
||||||
icon: IconUserOff,
|
icon: IconLock
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Not Found',
|
title: "Forbidden",
|
||||||
url: '/404',
|
url: "/403",
|
||||||
icon: IconError404,
|
icon: IconUserOff
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Internal Server Error',
|
title: "Not Found",
|
||||||
url: '/500',
|
url: "/404",
|
||||||
icon: IconServerOff,
|
icon: IconError404
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Maintenance Error',
|
title: "Internal Server Error",
|
||||||
url: '/503',
|
url: "/500",
|
||||||
icon: IconBarrierBlock,
|
icon: IconServerOff
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
},
|
title: "Maintenance Error",
|
||||||
],
|
url: "/503",
|
||||||
},
|
icon: IconBarrierBlock
|
||||||
{
|
}
|
||||||
title: 'Other',
|
]
|
||||||
items: [
|
}
|
||||||
{
|
]
|
||||||
title: 'Settings',
|
},
|
||||||
icon: IconSettings,
|
{
|
||||||
items: [
|
title: "Other",
|
||||||
{
|
items: [
|
||||||
title: 'Profile',
|
{
|
||||||
url: '/settings',
|
title: "Settings",
|
||||||
icon: IconUserCog,
|
icon: IconSettings,
|
||||||
},
|
items: [
|
||||||
{
|
{
|
||||||
title: 'Account',
|
title: "Profile",
|
||||||
url: '/settings/account',
|
url: "/settings",
|
||||||
icon: IconTool,
|
icon: IconUserCog
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Appearance',
|
title: "Account",
|
||||||
url: '/settings/appearance',
|
url: "/settings/account",
|
||||||
icon: IconPalette,
|
icon: IconTool
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Notifications',
|
title: "Appearance",
|
||||||
url: '/settings/notifications',
|
url: "/settings/appearance",
|
||||||
icon: IconNotification,
|
icon: IconPalette
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Display',
|
title: "Notifications",
|
||||||
url: '/settings/display',
|
url: "/settings/notifications",
|
||||||
icon: IconBrowserCheck,
|
icon: IconNotification
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
},
|
title: "Display",
|
||||||
{
|
url: "/settings/display",
|
||||||
title: 'Help Center',
|
icon: IconBrowserCheck
|
||||||
url: '/help-center',
|
}
|
||||||
icon: IconHelp,
|
]
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
},
|
title: "Help Center",
|
||||||
],
|
url: "/help-center",
|
||||||
}
|
icon: IconHelp
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -1,48 +1,47 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
|
||||||
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
|
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
fixed?: boolean
|
fixed?: boolean;
|
||||||
ref?: React.Ref<HTMLElement>
|
ref?: React.Ref<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Header = ({
|
export const Header = ({
|
||||||
className,
|
className,
|
||||||
fixed,
|
fixed,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: HeaderProps) => {
|
}: HeaderProps) => {
|
||||||
const [offset, setOffset] = React.useState(0)
|
const [offset, setOffset] = React.useState(0);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
setOffset(document.body.scrollTop || document.documentElement.scrollTop)
|
setOffset(document.body.scrollTop || document.documentElement.scrollTop);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Add scroll listener to the body
|
// Add scroll listener to the body
|
||||||
document.addEventListener('scroll', onScroll, { passive: true })
|
document.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
|
||||||
// Clean up the event listener on unmount
|
// Clean up the event listener on unmount
|
||||||
return () => document.removeEventListener('scroll', onScroll)
|
return () => document.removeEventListener("scroll", onScroll);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-16 items-center gap-3 bg-background p-4 sm:gap-4',
|
"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',
|
fixed && "header-fixed peer/header fixed z-50 w-[inherit] rounded-md",
|
||||||
offset > 10 && fixed ? 'shadow' : 'shadow-none',
|
offset > 10 && fixed ? "shadow" : "shadow-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<SidebarTrigger variant="outline" className="scale-125 sm:scale-100" />
|
||||||
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
|
<Separator orientation="vertical" className="h-6" />
|
||||||
<Separator orientation='vertical' className='h-6' />
|
{children}
|
||||||
{children}
|
</header>
|
||||||
</header>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
Header.displayName = 'Header'
|
Header.displayName = "Header";
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface MainProps extends React.HTMLAttributes<HTMLElement> {
|
interface MainProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
fixed?: boolean
|
fixed?: boolean;
|
||||||
ref?: React.Ref<HTMLElement>
|
ref?: React.Ref<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Main = ({ fixed, ...props }: MainProps) => {
|
export const Main = ({ fixed, ...props }: MainProps) => {
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className={cn(
|
className={cn(
|
||||||
'peer-[.header-fixed]/header:mt-16',
|
"peer-[.header-fixed]/header:mt-16",
|
||||||
'px-4 py-6',
|
"px-4 py-6",
|
||||||
fixed && 'fixed-main flex flex-grow flex-col overflow-hidden'
|
fixed && "fixed-main flex flex-grow flex-col overflow-hidden"
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Main.displayName = 'Main'
|
Main.displayName = "Main";
|
||||||
@@ -1,180 +1,175 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from "react";
|
||||||
import { Link, useLocation } from '@tanstack/react-router'
|
import { Link, useLocation } from "@tanstack/react-router";
|
||||||
import { ChevronRight } from 'lucide-react'
|
import { ChevronRight } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger
|
||||||
} from '@/components/ui/collapsible'
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
useSidebar,
|
useSidebar
|
||||||
} from '@/components/ui/sidebar'
|
} from "@/components/ui/sidebar";
|
||||||
import { Badge } from '../ui/badge'
|
import { Badge } from "../ui/badge";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger
|
||||||
} from '../ui/dropdown-menu'
|
} from "../ui/dropdown-menu";
|
||||||
import { NavCollapsible, NavItem, NavLink, type NavGroup } from './types'
|
import { NavCollapsible, NavItem, NavLink, type NavGroup } from "./types";
|
||||||
|
|
||||||
export function NavGroup({ title, items }: NavGroup) {
|
export function NavGroup({ title, items }: NavGroup) {
|
||||||
const { state } = useSidebar()
|
const { state } = useSidebar();
|
||||||
const href = useLocation({ select: (location) => location.href })
|
const href = useLocation({ select: (location) => location.href });
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>{title}</SidebarGroupLabel>
|
<SidebarGroupLabel>{title}</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const key = `${item.title}-${item.url}`
|
const key = `${item.title}-${item.url}`;
|
||||||
|
|
||||||
if (!item.items)
|
if (!item.items)
|
||||||
return <SidebarMenuLink key={key} item={item} href={href} />
|
return <SidebarMenuLink key={key} item={item} href={href} />;
|
||||||
|
|
||||||
if (state === 'collapsed')
|
if (state === "collapsed")
|
||||||
return (
|
return (
|
||||||
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
|
<SidebarMenuCollapsedDropdown key={key} item={item} href={href} />
|
||||||
)
|
);
|
||||||
|
|
||||||
return <SidebarMenuCollapsible key={key} item={item} href={href} />
|
return <SidebarMenuCollapsible key={key} item={item} href={href} />;
|
||||||
})}
|
})}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavBadge = ({ children }: { children: ReactNode }) => (
|
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 SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => {
|
||||||
const { setOpenMobile } = useSidebar()
|
const { setOpenMobile } = useSidebar();
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
isActive={checkIsActive(href, item)}
|
isActive={checkIsActive(href, item)}
|
||||||
tooltip={item.title}
|
tooltip={item.title}>
|
||||||
>
|
<Link to={item.url} onClick={() => setOpenMobile(false)}>
|
||||||
<Link to={item.url} onClick={() => setOpenMobile(false)}>
|
{item.icon && <item.icon />}
|
||||||
{item.icon && <item.icon />}
|
<span>{item.title}</span>
|
||||||
<span>{item.title}</span>
|
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
</Link>
|
||||||
</Link>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuItem>
|
||||||
</SidebarMenuItem>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarMenuCollapsible = ({
|
const SidebarMenuCollapsible = ({
|
||||||
item,
|
item,
|
||||||
href,
|
href
|
||||||
}: {
|
}: {
|
||||||
item: NavCollapsible
|
item: NavCollapsible;
|
||||||
href: string
|
href: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { setOpenMobile } = useSidebar()
|
const { setOpenMobile } = useSidebar();
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
asChild
|
asChild
|
||||||
defaultOpen={checkIsActive(href, item, true)}
|
defaultOpen={checkIsActive(href, item, true)}
|
||||||
className='group/collapsible'
|
className="group/collapsible">
|
||||||
>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuItem>
|
<CollapsibleTrigger asChild>
|
||||||
<CollapsibleTrigger asChild>
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
{item.icon && <item.icon />}
|
||||||
{item.icon && <item.icon />}
|
<span>{item.title}</span>
|
||||||
<span>{item.title}</span>
|
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</CollapsibleTrigger>
|
||||||
</CollapsibleTrigger>
|
<CollapsibleContent className="CollapsibleContent">
|
||||||
<CollapsibleContent className='CollapsibleContent'>
|
<SidebarMenuSub>
|
||||||
<SidebarMenuSub>
|
{item.items.map((subItem) => (
|
||||||
{item.items.map((subItem) => (
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
<SidebarMenuSubButton
|
||||||
<SidebarMenuSubButton
|
asChild
|
||||||
asChild
|
isActive={checkIsActive(href, subItem)}>
|
||||||
isActive={checkIsActive(href, subItem)}
|
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
||||||
>
|
{subItem.icon && <subItem.icon />}
|
||||||
<Link to={subItem.url} onClick={() => setOpenMobile(false)}>
|
<span>{subItem.title}</span>
|
||||||
{subItem.icon && <subItem.icon />}
|
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
|
||||||
<span>{subItem.title}</span>
|
</Link>
|
||||||
{subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
|
</SidebarMenuSubButton>
|
||||||
</Link>
|
</SidebarMenuSubItem>
|
||||||
</SidebarMenuSubButton>
|
))}
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSub>
|
||||||
))}
|
</CollapsibleContent>
|
||||||
</SidebarMenuSub>
|
</SidebarMenuItem>
|
||||||
</CollapsibleContent>
|
</Collapsible>
|
||||||
</SidebarMenuItem>
|
);
|
||||||
</Collapsible>
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarMenuCollapsedDropdown = ({
|
const SidebarMenuCollapsedDropdown = ({
|
||||||
item,
|
item,
|
||||||
href,
|
href
|
||||||
}: {
|
}: {
|
||||||
item: NavCollapsible
|
item: NavCollapsible;
|
||||||
href: string
|
href: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
tooltip={item.title}
|
tooltip={item.title}
|
||||||
isActive={checkIsActive(href, item)}
|
isActive={checkIsActive(href, item)}>
|
||||||
>
|
{item.icon && <item.icon />}
|
||||||
{item.icon && <item.icon />}
|
<span>{item.title}</span>
|
||||||
<span>{item.title}</span>
|
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
||||||
{item.badge && <NavBadge>{item.badge}</NavBadge>}
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
<ChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent side="right" align="start" sideOffset={4}>
|
||||||
<DropdownMenuContent side='right' align='start' sideOffset={4}>
|
<DropdownMenuLabel>
|
||||||
<DropdownMenuLabel>
|
{item.title} {item.badge ? `(${item.badge})` : ""}
|
||||||
{item.title} {item.badge ? `(${item.badge})` : ''}
|
</DropdownMenuLabel>
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuSeparator />
|
{item.items.map((sub) => (
|
||||||
{item.items.map((sub) => (
|
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
|
||||||
<DropdownMenuItem key={`${sub.title}-${sub.url}`} asChild>
|
<Link
|
||||||
<Link
|
to={sub.url}
|
||||||
to={sub.url}
|
className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}>
|
||||||
className={`${checkIsActive(href, sub) ? 'bg-secondary' : ''}`}
|
{sub.icon && <sub.icon />}
|
||||||
>
|
<span className="max-w-52 text-wrap">{sub.title}</span>
|
||||||
{sub.icon && <sub.icon />}
|
{sub.badge && (
|
||||||
<span className='max-w-52 text-wrap'>{sub.title}</span>
|
<span className="ml-auto text-xs">{sub.badge}</span>
|
||||||
{sub.badge && (
|
)}
|
||||||
<span className='ml-auto text-xs'>{sub.badge}</span>
|
</Link>
|
||||||
)}
|
</DropdownMenuItem>
|
||||||
</Link>
|
))}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuContent>
|
||||||
))}
|
</DropdownMenu>
|
||||||
</DropdownMenuContent>
|
</SidebarMenuItem>
|
||||||
</DropdownMenu>
|
);
|
||||||
</SidebarMenuItem>
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkIsActive(href: string, item: NavItem, mainNav = false) {
|
function checkIsActive(href: string, item: NavItem, mainNav = false) {
|
||||||
return (
|
return (
|
||||||
href === item.url || // /endpint?search=param
|
href === item.url || // /endpint?search=param
|
||||||
href.split('?')[0] === item.url || // endpoint
|
href.split("?")[0] === item.url || // endpoint
|
||||||
!!item?.items?.filter((i) => i.url === href).length || // if child nav is active
|
!!item?.items?.filter((i) => i.url === href).length || // if child nav is active
|
||||||
(mainNav &&
|
(mainNav &&
|
||||||
href.split('/')[1] !== '' &&
|
href.split("/")[1] !== "" &&
|
||||||
href.split('/')[1] === item?.url?.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 {
|
import {
|
||||||
BadgeCheck,
|
BadgeCheck,
|
||||||
Bell,
|
Bell,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
LogOut,
|
LogOut,
|
||||||
Sparkles,
|
Sparkles
|
||||||
} from 'lucide-react'
|
} from "lucide-react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar
|
||||||
} from '@/components/ui/sidebar'
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user,
|
user
|
||||||
}: {
|
}: {
|
||||||
user: {
|
user: {
|
||||||
name: string
|
name: string;
|
||||||
email: string
|
email: string;
|
||||||
avatar: string
|
avatar: string;
|
||||||
}
|
};
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size='lg'
|
size="lg"
|
||||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||||
>
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<Avatar className='h-8 w-8 rounded-lg'>
|
<AvatarImage src={user.avatar} alt={user.name} />
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
<AvatarFallback className="rounded-lg">SN</AvatarFallback>
|
||||||
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
|
</Avatar>
|
||||||
</Avatar>
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<div className='grid flex-1 text-left text-sm leading-tight'>
|
<span className="truncate font-semibold">{user.name}</span>
|
||||||
<span className='truncate font-semibold'>{user.name}</span>
|
<span className="truncate text-xs">{user.email}</span>
|
||||||
<span className='truncate text-xs'>{user.email}</span>
|
</div>
|
||||||
</div>
|
<ChevronsUpDown className="ml-auto size-4" />
|
||||||
<ChevronsUpDown className='ml-auto size-4' />
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent
|
||||||
<DropdownMenuContent
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
className='w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg'
|
side={isMobile ? "bottom" : "right"}
|
||||||
side={isMobile ? 'bottom' : 'right'}
|
align="end"
|
||||||
align='end'
|
sideOffset={4}>
|
||||||
sideOffset={4}
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
>
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<DropdownMenuLabel className='p-0 font-normal'>
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<div className='flex items-center gap-2 px-1 py-1.5 text-left text-sm'>
|
<AvatarImage src={user.avatar} alt={user.name} />
|
||||||
<Avatar className='h-8 w-8 rounded-lg'>
|
<AvatarFallback className="rounded-lg">SN</AvatarFallback>
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
</Avatar>
|
||||||
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
</Avatar>
|
<span className="truncate font-semibold">{user.name}</span>
|
||||||
<div className='grid flex-1 text-left text-sm leading-tight'>
|
<span className="truncate text-xs">{user.email}</span>
|
||||||
<span className='truncate font-semibold'>{user.name}</span>
|
</div>
|
||||||
<span className='truncate text-xs'>{user.email}</span>
|
</div>
|
||||||
</div>
|
</DropdownMenuLabel>
|
||||||
</div>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem>
|
||||||
<DropdownMenuGroup>
|
<Sparkles />
|
||||||
<DropdownMenuItem>
|
Upgrade to Pro
|
||||||
<Sparkles />
|
</DropdownMenuItem>
|
||||||
Upgrade to Pro
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem asChild>
|
||||||
<DropdownMenuGroup>
|
<Link to="/settings/account">
|
||||||
<DropdownMenuItem asChild>
|
<BadgeCheck />
|
||||||
<Link to='/settings/account'>
|
Account
|
||||||
<BadgeCheck />
|
</Link>
|
||||||
Account
|
</DropdownMenuItem>
|
||||||
</Link>
|
<DropdownMenuItem asChild>
|
||||||
</DropdownMenuItem>
|
<Link to="/settings">
|
||||||
<DropdownMenuItem asChild>
|
<CreditCard />
|
||||||
<Link to='/settings'>
|
Billing
|
||||||
<CreditCard />
|
</Link>
|
||||||
Billing
|
</DropdownMenuItem>
|
||||||
</Link>
|
<DropdownMenuItem asChild>
|
||||||
</DropdownMenuItem>
|
<Link to="/settings/notifications">
|
||||||
<DropdownMenuItem asChild>
|
<Bell />
|
||||||
<Link to='/settings/notifications'>
|
Notifications
|
||||||
<Bell />
|
</Link>
|
||||||
Notifications
|
</DropdownMenuItem>
|
||||||
</Link>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuGroup>
|
<DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<LogOut />
|
||||||
<DropdownMenuItem>
|
Log out
|
||||||
<LogOut />
|
</DropdownMenuItem>
|
||||||
Log out
|
</DropdownMenuContent>
|
||||||
</DropdownMenuItem>
|
</DropdownMenu>
|
||||||
</DropdownMenuContent>
|
</SidebarMenuItem>
|
||||||
</DropdownMenu>
|
</SidebarMenu>
|
||||||
</SidebarMenuItem>
|
);
|
||||||
</SidebarMenu>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,86 +1,83 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { ChevronsUpDown, Plus } from 'lucide-react'
|
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar
|
||||||
} from '@/components/ui/sidebar'
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function TeamSwitcher({
|
export function TeamSwitcher({
|
||||||
teams,
|
teams
|
||||||
}: {
|
}: {
|
||||||
teams: {
|
teams: {
|
||||||
name: string
|
name: string;
|
||||||
logo: React.ElementType
|
logo: React.ElementType;
|
||||||
plan: string
|
plan: string;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
const [activeTeam, setActiveTeam] = React.useState(teams[0])
|
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size='lg'
|
size="lg"
|
||||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||||
>
|
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||||
<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" />
|
||||||
<activeTeam.logo className='size-4' />
|
</div>
|
||||||
</div>
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<div className='grid flex-1 text-left text-sm leading-tight'>
|
<span className="truncate font-semibold">
|
||||||
<span className='truncate font-semibold'>
|
{activeTeam.name}
|
||||||
{activeTeam.name}
|
</span>
|
||||||
</span>
|
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||||
<span className='truncate text-xs'>{activeTeam.plan}</span>
|
</div>
|
||||||
</div>
|
<ChevronsUpDown className="ml-auto" />
|
||||||
<ChevronsUpDown className='ml-auto' />
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent
|
||||||
<DropdownMenuContent
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
className='w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg'
|
align="start"
|
||||||
align='start'
|
side={isMobile ? "bottom" : "right"}
|
||||||
side={isMobile ? 'bottom' : 'right'}
|
sideOffset={4}>
|
||||||
sideOffset={4}
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
>
|
Teams
|
||||||
<DropdownMenuLabel className='text-xs text-muted-foreground'>
|
</DropdownMenuLabel>
|
||||||
Teams
|
{teams.map((team, index) => (
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuItem
|
||||||
{teams.map((team, index) => (
|
key={team.name}
|
||||||
<DropdownMenuItem
|
onClick={() => setActiveTeam(team)}
|
||||||
key={team.name}
|
className="gap-2 p-2">
|
||||||
onClick={() => setActiveTeam(team)}
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
className='gap-2 p-2'
|
<team.logo className="size-4 shrink-0" />
|
||||||
>
|
</div>
|
||||||
<div className='flex size-6 items-center justify-center rounded-sm border'>
|
{team.name}
|
||||||
<team.logo className='size-4 shrink-0' />
|
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
{team.name}
|
))}
|
||||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem className="gap-2 p-2">
|
||||||
))}
|
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
||||||
<DropdownMenuSeparator />
|
<Plus className="size-4" />
|
||||||
<DropdownMenuItem className='gap-2 p-2'>
|
</div>
|
||||||
<div className='flex size-6 items-center justify-center rounded-md border bg-background'>
|
<div className="font-medium text-muted-foreground">Add team</div>
|
||||||
<Plus className='size-4' />
|
</DropdownMenuItem>
|
||||||
</div>
|
</DropdownMenuContent>
|
||||||
<div className='font-medium text-muted-foreground'>Add team</div>
|
</DropdownMenu>
|
||||||
</DropdownMenuItem>
|
</SidebarMenuItem>
|
||||||
</DropdownMenuContent>
|
</SidebarMenu>
|
||||||
</DropdownMenu>
|
);
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,67 +1,64 @@
|
|||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from "@tanstack/react-router";
|
||||||
import { IconMenu } from '@tabler/icons-react'
|
import { IconMenu } from "@tabler/icons-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
|
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
links: {
|
links: {
|
||||||
title: string
|
title: string;
|
||||||
href: string
|
href: string;
|
||||||
isActive: boolean
|
isActive: boolean;
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
}[]
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopNav({ className, links, ...props }: TopNavProps) {
|
export function TopNav({ className, links, ...props }: TopNavProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='md:hidden'>
|
<div className="md:hidden">
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button size='icon' variant='outline'>
|
<Button size="icon" variant="outline">
|
||||||
<IconMenu />
|
<IconMenu />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side='bottom' align='start'>
|
<DropdownMenuContent side="bottom" align="start">
|
||||||
{links.map(({ title, href, isActive, disabled }) => (
|
{links.map(({ title, href, isActive, disabled }) => (
|
||||||
<DropdownMenuItem key={`${title}-${href}`} asChild>
|
<DropdownMenuItem key={`${title}-${href}`} asChild>
|
||||||
<Link
|
<Link
|
||||||
to={href}
|
to={href}
|
||||||
className={!isActive ? 'text-muted-foreground' : ''}
|
className={!isActive ? "text-muted-foreground" : ""}
|
||||||
disabled={disabled}
|
disabled={disabled}>
|
||||||
>
|
{title}
|
||||||
{title}
|
</Link>
|
||||||
</Link>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
))}
|
||||||
))}
|
</DropdownMenuContent>
|
||||||
</DropdownMenuContent>
|
</DropdownMenu>
|
||||||
</DropdownMenu>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
className={cn(
|
className={cn(
|
||||||
'hidden items-center space-x-4 md:flex lg:space-x-6',
|
"hidden items-center space-x-4 md:flex lg:space-x-6",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
{links.map(({ title, href, isActive, disabled }) => (
|
||||||
{links.map(({ title, href, isActive, disabled }) => (
|
<Link
|
||||||
<Link
|
key={`${title}-${href}`}
|
||||||
key={`${title}-${href}`}
|
to={href}
|
||||||
to={href}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? "" : "text-muted-foreground"}`}>
|
||||||
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? '' : 'text-muted-foreground'}`}
|
{title}
|
||||||
>
|
</Link>
|
||||||
{title}
|
))}
|
||||||
</Link>
|
</nav>
|
||||||
))}
|
</>
|
||||||
</nav>
|
);
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,44 +1,44 @@
|
|||||||
import { LinkProps } from '@tanstack/react-router'
|
import { LinkProps } from "@tanstack/react-router";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
name: string
|
name: string;
|
||||||
email: string
|
email: string;
|
||||||
avatar: string
|
avatar: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Team {
|
interface Team {
|
||||||
name: string
|
name: string;
|
||||||
logo: React.ElementType
|
logo: React.ElementType;
|
||||||
plan: string
|
plan: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseNavItem {
|
interface BaseNavItem {
|
||||||
title: string
|
title: string;
|
||||||
badge?: string
|
badge?: string;
|
||||||
icon?: React.ElementType
|
icon?: React.ElementType;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavLink = BaseNavItem & {
|
type NavLink = BaseNavItem & {
|
||||||
url: LinkProps['to']
|
url: LinkProps["to"];
|
||||||
items?: never
|
items?: never;
|
||||||
}
|
};
|
||||||
|
|
||||||
type NavCollapsible = BaseNavItem & {
|
type NavCollapsible = BaseNavItem & {
|
||||||
items: (BaseNavItem & { url: LinkProps['to'] })[]
|
items: (BaseNavItem & { url: LinkProps["to"] })[];
|
||||||
url?: never
|
url?: never;
|
||||||
}
|
};
|
||||||
|
|
||||||
type NavItem = NavCollapsible | NavLink
|
type NavItem = NavCollapsible | NavLink;
|
||||||
|
|
||||||
interface NavGroup {
|
interface NavGroup {
|
||||||
title: string
|
title: string;
|
||||||
items: NavItem[]
|
items: NavItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarData {
|
interface SidebarData {
|
||||||
user: User
|
user: User;
|
||||||
teams: Team[]
|
teams: Team[];
|
||||||
navGroups: NavGroup[]
|
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 { useEffect, useRef, useState } from "react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger
|
||||||
} from '@/components/ui/popover'
|
} from "@/components/ui/popover";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger
|
||||||
} from '@/components/ui/tooltip'
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
className?: string
|
className?: string;
|
||||||
contentClassName?: string
|
contentClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LongText({
|
export default function LongText({
|
||||||
children,
|
children,
|
||||||
className = '',
|
className = "",
|
||||||
contentClassName = '',
|
contentClassName = ""
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isOverflown, setIsOverflown] = useState(false)
|
const [isOverflown, setIsOverflown] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (checkOverflow(ref.current)) {
|
if (checkOverflow(ref.current)) {
|
||||||
setIsOverflown(true)
|
setIsOverflown(true);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsOverflown(false)
|
setIsOverflown(false);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!isOverflown)
|
if (!isOverflown)
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={cn('truncate', className)}>
|
<div ref={ref} className={cn("truncate", className)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='hidden sm:block'>
|
<div className="hidden sm:block">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div ref={ref} className={cn('truncate', className)}>
|
<div ref={ref} className={cn("truncate", className)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p className={contentClassName}>{children}</p>
|
<p className={contentClassName}>{children}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<div className='sm:hidden'>
|
<div className="sm:hidden">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div ref={ref} className={cn('truncate', className)}>
|
<div ref={ref} className={cn("truncate", className)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className={cn('w-fit', contentClassName)}>
|
<PopoverContent className={cn("w-fit", contentClassName)}>
|
||||||
<p>{children}</p>
|
<p>{children}</p>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkOverflow = (textContainer: HTMLDivElement | null) => {
|
const checkOverflow = (textContainer: HTMLDivElement | null) => {
|
||||||
if (textContainer) {
|
if (textContainer) {
|
||||||
return (
|
return (
|
||||||
textContainer.offsetHeight < textContainer.scrollHeight ||
|
textContainer.offsetHeight < textContainer.scrollHeight ||
|
||||||
textContainer.offsetWidth < textContainer.scrollWidth
|
textContainer.offsetWidth < textContainer.scrollWidth
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return false
|
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 {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger
|
||||||
} from "@/components/ui/collapsible"
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function NavMain({
|
export function NavMain({
|
||||||
items,
|
items
|
||||||
}: {
|
}: {
|
||||||
items: {
|
items: {
|
||||||
title: string
|
title: string;
|
||||||
url: string
|
url: string;
|
||||||
icon?: LucideIcon
|
icon?: LucideIcon;
|
||||||
isActive?: boolean
|
isActive?: boolean;
|
||||||
items?: {
|
items?: {
|
||||||
title: string
|
title: string;
|
||||||
url: string
|
url: string;
|
||||||
}[]
|
}[];
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
key={item.title}
|
key={item.title}
|
||||||
asChild
|
asChild
|
||||||
defaultOpen={item.isActive}
|
defaultOpen={item.isActive}
|
||||||
className="group/collapsible"
|
className="group/collapsible">
|
||||||
>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuItem>
|
<CollapsibleTrigger asChild>
|
||||||
<CollapsibleTrigger asChild>
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
{item.icon && <item.icon />}
|
||||||
{item.icon && <item.icon />}
|
<span>{item.title}</span>
|
||||||
<span>{item.title}</span>
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</CollapsibleTrigger>
|
||||||
</CollapsibleTrigger>
|
<CollapsibleContent>
|
||||||
<CollapsibleContent>
|
<SidebarMenuSub>
|
||||||
<SidebarMenuSub>
|
{item.items?.map((subItem) => (
|
||||||
{item.items?.map((subItem) => (
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
<SidebarMenuSubButton asChild>
|
||||||
<SidebarMenuSubButton asChild>
|
<a href={subItem.url}>
|
||||||
<a href={subItem.url}>
|
<span>{subItem.title}</span>
|
||||||
<span>{subItem.title}</span>
|
</a>
|
||||||
</a>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubItem>
|
||||||
</SidebarMenuSubItem>
|
))}
|
||||||
))}
|
</SidebarMenuSub>
|
||||||
</SidebarMenuSub>
|
</CollapsibleContent>
|
||||||
</CollapsibleContent>
|
</SidebarMenuItem>
|
||||||
</SidebarMenuItem>
|
</Collapsible>
|
||||||
</Collapsible>
|
))}
|
||||||
))}
|
</SidebarMenu>
|
||||||
</SidebarMenu>
|
</SidebarGroup>
|
||||||
</SidebarGroup>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,89 +1,88 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Folder,
|
Folder,
|
||||||
Forward,
|
Forward,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Trash2,
|
Trash2,
|
||||||
type LucideIcon,
|
type LucideIcon
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuAction,
|
SidebarMenuAction,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function NavProjects({
|
export function NavProjects({
|
||||||
projects,
|
projects
|
||||||
}: {
|
}: {
|
||||||
projects: {
|
projects: {
|
||||||
name: string
|
name: string;
|
||||||
url: string
|
url: string;
|
||||||
icon: LucideIcon
|
icon: LucideIcon;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{projects.map((item) => (
|
{projects.map((item) => (
|
||||||
<SidebarMenuItem key={item.name}>
|
<SidebarMenuItem key={item.name}>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarMenuButton asChild>
|
||||||
<a href={item.url}>
|
<a href={item.url}>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
<span>{item.name}</span>
|
<span>{item.name}</span>
|
||||||
</a>
|
</a>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuAction showOnHover>
|
<SidebarMenuAction showOnHover>
|
||||||
<MoreHorizontal />
|
<MoreHorizontal />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</SidebarMenuAction>
|
</SidebarMenuAction>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-48 rounded-lg"
|
className="w-48 rounded-lg"
|
||||||
side={isMobile ? "bottom" : "right"}
|
side={isMobile ? "bottom" : "right"}
|
||||||
align={isMobile ? "end" : "start"}
|
align={isMobile ? "end" : "start"}>
|
||||||
>
|
<DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<Folder className="text-muted-foreground" />
|
||||||
<Folder className="text-muted-foreground" />
|
<span>View Project</span>
|
||||||
<span>View Project</span>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<Forward className="text-muted-foreground" />
|
||||||
<Forward className="text-muted-foreground" />
|
<span>Share Project</span>
|
||||||
<span>Share Project</span>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<Trash2 className="text-muted-foreground" />
|
||||||
<Trash2 className="text-muted-foreground" />
|
<span>Delete Project</span>
|
||||||
<span>Delete Project</span>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuContent>
|
||||||
</DropdownMenuContent>
|
</DropdownMenu>
|
||||||
</DropdownMenu>
|
</SidebarMenuItem>
|
||||||
</SidebarMenuItem>
|
))}
|
||||||
))}
|
<SidebarMenuItem>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
<MoreHorizontal className="text-sidebar-foreground/70" />
|
||||||
<MoreHorizontal className="text-sidebar-foreground/70" />
|
<span>More</span>
|
||||||
<span>More</span>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuItem>
|
||||||
</SidebarMenuItem>
|
</SidebarMenu>
|
||||||
</SidebarMenu>
|
</SidebarGroup>
|
||||||
</SidebarGroup>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,114 +1,108 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BadgeCheck,
|
BadgeCheck,
|
||||||
Bell,
|
Bell,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
LogOut,
|
LogOut,
|
||||||
Sparkles,
|
Sparkles
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
DropdownMenu,
|
||||||
AvatarFallback,
|
DropdownMenuContent,
|
||||||
AvatarImage,
|
DropdownMenuGroup,
|
||||||
} from "@/components/ui/avatar"
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
SidebarMenu,
|
||||||
DropdownMenuContent,
|
SidebarMenuButton,
|
||||||
DropdownMenuGroup,
|
SidebarMenuItem,
|
||||||
DropdownMenuItem,
|
useSidebar
|
||||||
DropdownMenuLabel,
|
} from "@/components/ui/sidebar";
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu"
|
|
||||||
import {
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from "@/components/ui/sidebar"
|
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user,
|
user
|
||||||
}: {
|
}: {
|
||||||
user: {
|
user: {
|
||||||
name: string
|
name: string;
|
||||||
email: string
|
email: string;
|
||||||
avatar: string
|
avatar: string;
|
||||||
}
|
};
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
size="lg"
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||||
>
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<AvatarImage src={user.avatar} alt={user.name} />
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
</Avatar>
|
||||||
</Avatar>
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<span className="truncate font-semibold">{user.name}</span>
|
||||||
<span className="truncate font-semibold">{user.name}</span>
|
<span className="truncate text-xs">{user.email}</span>
|
||||||
<span className="truncate text-xs">{user.email}</span>
|
</div>
|
||||||
</div>
|
<ChevronsUpDown className="ml-auto size-4" />
|
||||||
<ChevronsUpDown className="ml-auto size-4" />
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent
|
||||||
<DropdownMenuContent
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
side={isMobile ? "bottom" : "right"}
|
||||||
side={isMobile ? "bottom" : "right"}
|
align="end"
|
||||||
align="end"
|
sideOffset={4}>
|
||||||
sideOffset={4}
|
<DropdownMenuLabel className="p-0 font-normal">
|
||||||
>
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<AvatarImage src={user.avatar} alt={user.name} />
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
</Avatar>
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
</Avatar>
|
<span className="truncate font-semibold">{user.name}</span>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<span className="truncate text-xs">{user.email}</span>
|
||||||
<span className="truncate font-semibold">{user.name}</span>
|
</div>
|
||||||
<span className="truncate text-xs">{user.email}</span>
|
</div>
|
||||||
</div>
|
</DropdownMenuLabel>
|
||||||
</div>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem>
|
||||||
<DropdownMenuGroup>
|
<Sparkles />
|
||||||
<DropdownMenuItem>
|
Upgrade to Pro
|
||||||
<Sparkles />
|
</DropdownMenuItem>
|
||||||
Upgrade to Pro
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuItem>
|
||||||
<DropdownMenuGroup>
|
<BadgeCheck />
|
||||||
<DropdownMenuItem>
|
Account
|
||||||
<BadgeCheck />
|
</DropdownMenuItem>
|
||||||
Account
|
<DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<CreditCard />
|
||||||
<DropdownMenuItem>
|
Billing
|
||||||
<CreditCard />
|
</DropdownMenuItem>
|
||||||
Billing
|
<DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<Bell />
|
||||||
<DropdownMenuItem>
|
Notifications
|
||||||
<Bell />
|
</DropdownMenuItem>
|
||||||
Notifications
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuGroup>
|
<DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<LogOut />
|
||||||
<DropdownMenuItem>
|
Log out
|
||||||
<LogOut />
|
</DropdownMenuItem>
|
||||||
Log out
|
</DropdownMenuContent>
|
||||||
</DropdownMenuItem>
|
</DropdownMenu>
|
||||||
</DropdownMenuContent>
|
</SidebarMenuItem>
|
||||||
</DropdownMenu>
|
</SidebarMenu>
|
||||||
</SidebarMenuItem>
|
);
|
||||||
</SidebarMenu>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,38 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { IconEye, IconEyeOff } from '@tabler/icons-react'
|
import { IconEye, IconEyeOff } from "@tabler/icons-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from './ui/button'
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
type PasswordInputProps = Omit<
|
type PasswordInputProps = Omit<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
'type'
|
"type"
|
||||||
>
|
>;
|
||||||
|
|
||||||
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||||
({ className, disabled, ...props }, ref) => {
|
({ className, disabled, ...props }, ref) => {
|
||||||
const [showPassword, setShowPassword] = React.useState(false)
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative rounded-md', className)}>
|
<div className={cn("relative rounded-md", className)}>
|
||||||
<input
|
<input
|
||||||
type={showPassword ? 'text' : 'password'}
|
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'
|
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}
|
ref={ref}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type="button"
|
||||||
size='icon'
|
size="icon"
|
||||||
variant='ghost'
|
variant="ghost"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className='absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground'
|
className="absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground"
|
||||||
onClick={() => setShowPassword((prev) => !prev)}
|
onClick={() => setShowPassword((prev) => !prev)}>
|
||||||
>
|
{showPassword ? <IconEye size={18} /> : <IconEyeOff size={18} />}
|
||||||
{showPassword ? <IconEye size={18} /> : <IconEyeOff size={18} />}
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
}
|
||||||
}
|
);
|
||||||
)
|
PasswordInput.displayName = "PasswordInput";
|
||||||
PasswordInput.displayName = 'PasswordInput'
|
|
||||||
|
|
||||||
export { PasswordInput }
|
export { PasswordInput };
|
||||||
@@ -1,454 +1,454 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface PinInputProps {
|
interface PinInputProps {
|
||||||
children:
|
children:
|
||||||
| React.ReactElement<typeof PinInputField>
|
| React.ReactElement<typeof PinInputField>
|
||||||
| React.ReactElement<typeof PinInputField>[]
|
| React.ReactElement<typeof PinInputField>[];
|
||||||
/**
|
/**
|
||||||
* className for the input container
|
* className for the input container
|
||||||
*/
|
*/
|
||||||
className?: string
|
className?: string;
|
||||||
/**
|
/**
|
||||||
* `aria-label` for the input fields
|
* `aria-label` for the input fields
|
||||||
*/
|
*/
|
||||||
ariaLabel?: string
|
ariaLabel?: string;
|
||||||
/**
|
/**
|
||||||
* If set, the pin input receives focus on mount, `false` by default
|
* If set, the pin input receives focus on mount, `false` by default
|
||||||
*/
|
*/
|
||||||
autoFocus?: boolean
|
autoFocus?: boolean;
|
||||||
/**
|
/**
|
||||||
* Called when value changes
|
* Called when value changes
|
||||||
*/
|
*/
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void;
|
||||||
/**
|
/**
|
||||||
* Called when all inputs have valid value
|
* Called when all inputs have valid value
|
||||||
*/
|
*/
|
||||||
onComplete?: (value: string) => void
|
onComplete?: (value: string) => void;
|
||||||
/**
|
/**
|
||||||
* Called when any input doesn't have value
|
* Called when any input doesn't have value
|
||||||
*/
|
*/
|
||||||
onIncomplete?: (value: string) => void
|
onIncomplete?: (value: string) => void;
|
||||||
/**
|
/**
|
||||||
* `name` attribute for input fields
|
* `name` attribute for input fields
|
||||||
*/
|
*/
|
||||||
name?: string
|
name?: string;
|
||||||
/**
|
/**
|
||||||
* `form` attribute for hidden input
|
* `form` attribute for hidden input
|
||||||
*/
|
*/
|
||||||
form?: string
|
form?: string;
|
||||||
/**
|
/**
|
||||||
* If set, the input's value will be masked just like password input. This field is `false` by default
|
* If set, the input's value will be masked just like password input. This field is `false` by default
|
||||||
*/
|
*/
|
||||||
mask?: boolean
|
mask?: boolean;
|
||||||
/**
|
/**
|
||||||
* If set, the pin input component signals to its fields that they should
|
* If set, the pin input component signals to its fields that they should
|
||||||
* use `autocomplete="one-time-code"`. This field is `false` by default
|
* use `autocomplete="one-time-code"`. This field is `false` by default
|
||||||
*/
|
*/
|
||||||
otp?: boolean
|
otp?: boolean;
|
||||||
/**
|
/**
|
||||||
* Uncontrolled pin input default value.
|
* Uncontrolled pin input default value.
|
||||||
*/
|
*/
|
||||||
defaultValue?: string
|
defaultValue?: string;
|
||||||
/**
|
/**
|
||||||
* Controlled pin input value.
|
* Controlled pin input value.
|
||||||
*/
|
*/
|
||||||
value?: string
|
value?: string;
|
||||||
/**
|
/**
|
||||||
* The type of value pin input should allow, `alphanumeric` by default
|
* The type of value pin input should allow, `alphanumeric` by default
|
||||||
*/
|
*/
|
||||||
type?: 'numeric' | 'alphanumeric'
|
type?: "numeric" | "alphanumeric";
|
||||||
/**
|
/**
|
||||||
* Placeholder for input fields, `○` by default
|
* Placeholder for input fields, `○` by default
|
||||||
*/
|
*/
|
||||||
placeholder?: string
|
placeholder?: string;
|
||||||
/**
|
/**
|
||||||
* If set, the user cannot set the value, `false` by default
|
* If set, the user cannot set the value, `false` by default
|
||||||
*/
|
*/
|
||||||
readOnly?: boolean
|
readOnly?: boolean;
|
||||||
/**
|
/**
|
||||||
* If set, the input fields are disabled, `false` by default
|
* If set, the input fields are disabled, `false` by default
|
||||||
*/
|
*/
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
ref?: React.Ref<HTMLDivElement>
|
ref?: React.Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PinInputContext = React.createContext<boolean>(false)
|
const PinInputContext = React.createContext<boolean>(false);
|
||||||
|
|
||||||
const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
|
const PinInput = ({ className, children, ref, ...props }: PinInputProps) => {
|
||||||
const {
|
const {
|
||||||
defaultValue,
|
defaultValue,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onComplete,
|
onComplete,
|
||||||
onIncomplete,
|
onIncomplete,
|
||||||
placeholder = '○',
|
placeholder = "○",
|
||||||
type = 'alphanumeric',
|
type = "alphanumeric",
|
||||||
name,
|
name,
|
||||||
form,
|
form,
|
||||||
otp = false,
|
otp = false,
|
||||||
mask = false,
|
mask = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
ariaLabel = '',
|
ariaLabel = "",
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props;
|
||||||
|
|
||||||
const validChildren = getValidChildren(children)
|
const validChildren = getValidChildren(children);
|
||||||
|
|
||||||
const length = getInputFieldCount(children)
|
const length = getInputFieldCount(children);
|
||||||
|
|
||||||
// pins, pinValue, refMap, ...handlers
|
// pins, pinValue, refMap, ...handlers
|
||||||
const { pins, pinValue, refMap, ...handlers } = usePinInput({
|
const { pins, pinValue, refMap, ...handlers } = usePinInput({
|
||||||
value,
|
value,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
placeholder,
|
placeholder,
|
||||||
type,
|
type,
|
||||||
length,
|
length,
|
||||||
readOnly,
|
readOnly
|
||||||
})
|
});
|
||||||
|
|
||||||
/* call onChange func if pinValue changes */
|
/* call onChange func if pinValue changes */
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!onChange) return
|
if (!onChange) return;
|
||||||
onChange(pinValue)
|
onChange(pinValue);
|
||||||
}, [onChange, pinValue])
|
}, [onChange, pinValue]);
|
||||||
|
|
||||||
/* call onComplete/onIncomplete func if pinValue is valid and completed/incompleted */
|
/* call onComplete/onIncomplete func if pinValue is valid and completed/incompleted */
|
||||||
const completeRef = React.useRef(pinValue.length === length)
|
const completeRef = React.useRef(pinValue.length === length);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (pinValue.length === length && completeRef.current === false) {
|
if (pinValue.length === length && completeRef.current === false) {
|
||||||
completeRef.current = true
|
completeRef.current = true;
|
||||||
if (onComplete) onComplete(pinValue)
|
if (onComplete) onComplete(pinValue);
|
||||||
}
|
}
|
||||||
if (pinValue.length !== length && completeRef.current === true) {
|
if (pinValue.length !== length && completeRef.current === true) {
|
||||||
completeRef.current = false
|
completeRef.current = false;
|
||||||
if (onIncomplete) onIncomplete(pinValue)
|
if (onIncomplete) onIncomplete(pinValue);
|
||||||
}
|
}
|
||||||
}, [length, onComplete, onIncomplete, pinValue, pins, value])
|
}, [length, onComplete, onIncomplete, pinValue, pins, value]);
|
||||||
|
|
||||||
/* focus on first input field if autoFocus is set */
|
/* focus on first input field if autoFocus is set */
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!autoFocus) return
|
if (!autoFocus) return;
|
||||||
const node = refMap?.get(0)
|
const node = refMap?.get(0);
|
||||||
if (node) {
|
if (node) {
|
||||||
node.focus()
|
node.focus();
|
||||||
}
|
}
|
||||||
}, [autoFocus, refMap])
|
}, [autoFocus, refMap]);
|
||||||
|
|
||||||
const skipRef = React.useRef(0)
|
const skipRef = React.useRef(0);
|
||||||
let counter = 0
|
let counter = 0;
|
||||||
const clones = validChildren.map((child) => {
|
const clones = validChildren.map((child) => {
|
||||||
if (child.type === PinInputField) {
|
if (child.type === PinInputField) {
|
||||||
const pinIndex = counter
|
const pinIndex = counter;
|
||||||
counter = counter + 1
|
counter = counter + 1;
|
||||||
return React.cloneElement(child, {
|
return React.cloneElement(child, {
|
||||||
name,
|
name,
|
||||||
inputKey: `input-${pinIndex}`,
|
inputKey: `input-${pinIndex}`,
|
||||||
value: length > pinIndex ? pins[pinIndex] : '',
|
value: length > pinIndex ? pins[pinIndex] : "",
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
handlers.handleChange(e, pinIndex),
|
handlers.handleChange(e, pinIndex),
|
||||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) =>
|
onFocus: (e: React.FocusEvent<HTMLInputElement>) =>
|
||||||
handlers.handleFocus(e, pinIndex),
|
handlers.handleFocus(e, pinIndex),
|
||||||
onBlur: () => handlers.handleBlur(pinIndex),
|
onBlur: () => handlers.handleBlur(pinIndex),
|
||||||
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) =>
|
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||||
handlers.handleKeyDown(e, pinIndex),
|
handlers.handleKeyDown(e, pinIndex),
|
||||||
onPaste: (e: React.ClipboardEvent<HTMLInputElement>) =>
|
onPaste: (e: React.ClipboardEvent<HTMLInputElement>) =>
|
||||||
handlers.handlePaste(e),
|
handlers.handlePaste(e),
|
||||||
placeholder: placeholder,
|
placeholder: placeholder,
|
||||||
type: type,
|
type: type,
|
||||||
mask: mask,
|
mask: mask,
|
||||||
autoComplete: otp ? 'one-time-code' : 'off',
|
autoComplete: otp ? "one-time-code" : "off",
|
||||||
disabled: disabled,
|
disabled: disabled,
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
'aria-label': ariaLabel
|
"aria-label": ariaLabel
|
||||||
? ariaLabel
|
? ariaLabel
|
||||||
: `Pin input ${counter} of ${length}`,
|
: `Pin input ${counter} of ${length}`,
|
||||||
ref: (node: HTMLInputElement | null) => {
|
ref: (node: HTMLInputElement | null) => {
|
||||||
if (node) {
|
if (node) {
|
||||||
refMap?.set(pinIndex, node)
|
refMap?.set(pinIndex, node);
|
||||||
} else {
|
} else {
|
||||||
refMap?.delete(pinIndex)
|
refMap?.delete(pinIndex);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
skipRef.current = skipRef.current + 1
|
skipRef.current = skipRef.current + 1;
|
||||||
return child
|
return child;
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PinInputContext.Provider value={true}>
|
<PinInputContext.Provider value={true}>
|
||||||
<div ref={ref} aria-label='Pin Input' className={className} {...rest}>
|
<div ref={ref} aria-label="Pin Input" className={className} {...rest}>
|
||||||
{clones}
|
{clones}
|
||||||
<input type='hidden' name={name} form={form} value={pinValue} />
|
<input type="hidden" name={name} form={form} value={pinValue} />
|
||||||
</div>
|
</div>
|
||||||
</PinInputContext.Provider>
|
</PinInputContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
PinInput.displayName = 'PinInput'
|
PinInput.displayName = "PinInput";
|
||||||
|
|
||||||
/* ========== PinInputField ========== */
|
/* ========== PinInputField ========== */
|
||||||
|
|
||||||
interface _PinInputFieldProps {
|
interface _PinInputFieldProps {
|
||||||
mask: boolean
|
mask: boolean;
|
||||||
inputKey: string
|
inputKey: string;
|
||||||
type: 'numeric' | 'alphanumeric'
|
type: "numeric" | "alphanumeric";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PinInputFieldProps<T>
|
interface PinInputFieldProps<T>
|
||||||
extends Omit<
|
extends Omit<
|
||||||
React.ComponentPropsWithoutRef<'input'>,
|
React.ComponentPropsWithoutRef<"input">,
|
||||||
keyof _PinInputFieldProps
|
keyof _PinInputFieldProps
|
||||||
> {
|
> {
|
||||||
component?: T
|
component?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PinInputField = <T extends React.ElementType = 'input'>({
|
const PinInputField = <T extends React.ElementType = "input">({
|
||||||
className,
|
className,
|
||||||
component,
|
component,
|
||||||
...props
|
...props
|
||||||
}: PinInputFieldProps<T> &
|
}: PinInputFieldProps<T> &
|
||||||
(React.ComponentType<T> extends undefined
|
(React.ComponentType<T> extends undefined
|
||||||
? never
|
? never
|
||||||
: React.ComponentProps<T>)) => {
|
: React.ComponentProps<T>)) => {
|
||||||
const { mask, type, inputKey, ...rest } = props as _PinInputFieldProps &
|
const { mask, type, inputKey, ...rest } = props as _PinInputFieldProps &
|
||||||
React.ComponentProps<T>
|
React.ComponentProps<T>;
|
||||||
|
|
||||||
// Check if PinInputField is used within PinInput
|
// Check if PinInputField is used within PinInput
|
||||||
const isInsidePinInput = React.useContext(PinInputContext)
|
const isInsidePinInput = React.useContext(PinInputContext);
|
||||||
if (!isInsidePinInput) {
|
if (!isInsidePinInput) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`PinInputField must be used within ${PinInput.displayName}.`
|
`PinInputField must be used within ${PinInput.displayName}.`
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Element = component || 'input'
|
const Element = component || "input";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Element
|
<Element
|
||||||
key={inputKey}
|
key={inputKey}
|
||||||
type={mask ? 'password' : type === 'numeric' ? 'tel' : 'text'}
|
type={mask ? "password" : type === "numeric" ? "tel" : "text"}
|
||||||
inputMode={type === 'numeric' ? 'numeric' : 'text'}
|
inputMode={type === "numeric" ? "numeric" : "text"}
|
||||||
className={cn('size-10 text-center', className)}
|
className={cn("size-10 text-center", className)}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
/* ========== usePinInput custom hook ========== */
|
/* ========== usePinInput custom hook ========== */
|
||||||
|
|
||||||
interface UsePinInputProps {
|
interface UsePinInputProps {
|
||||||
value: string | undefined
|
value: string | undefined;
|
||||||
defaultValue: string | undefined
|
defaultValue: string | undefined;
|
||||||
placeholder: string
|
placeholder: string;
|
||||||
type: 'numeric' | 'alphanumeric'
|
type: "numeric" | "alphanumeric";
|
||||||
length: number
|
length: number;
|
||||||
readOnly: boolean
|
readOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usePinInput = ({
|
const usePinInput = ({
|
||||||
value,
|
value,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
placeholder,
|
placeholder,
|
||||||
type,
|
type,
|
||||||
length,
|
length,
|
||||||
readOnly,
|
readOnly
|
||||||
}: UsePinInputProps) => {
|
}: UsePinInputProps) => {
|
||||||
const pinInputs = React.useMemo(
|
const pinInputs = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
Array.from({ length }, (_, index) =>
|
Array.from({ length }, (_, index) =>
|
||||||
defaultValue
|
defaultValue
|
||||||
? defaultValue.charAt(index)
|
? defaultValue.charAt(index)
|
||||||
: value
|
: value
|
||||||
? value.charAt(index)
|
? value.charAt(index)
|
||||||
: ''
|
: ""
|
||||||
),
|
),
|
||||||
[defaultValue, length, value]
|
[defaultValue, length, value]
|
||||||
)
|
);
|
||||||
|
|
||||||
const [pins, setPins] = React.useState(pinInputs)
|
const [pins, setPins] = React.useState(pinInputs);
|
||||||
const pinValue = pins.join('').trim()
|
const pinValue = pins.join("").trim();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update pins when values change programmatically.
|
* Update pins when values change programmatically.
|
||||||
* This syncs the pins if the `defaultValue` or `value` prop is updated.
|
* This syncs the pins if the `defaultValue` or `value` prop is updated.
|
||||||
*/
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setPins(pinInputs)
|
setPins(pinInputs);
|
||||||
}, [pinInputs])
|
}, [pinInputs]);
|
||||||
|
|
||||||
const itemsRef = React.useRef<Map<number, HTMLInputElement> | null>(null)
|
const itemsRef = React.useRef<Map<number, HTMLInputElement> | null>(null);
|
||||||
|
|
||||||
function getMap() {
|
function getMap() {
|
||||||
if (!itemsRef.current) {
|
if (!itemsRef.current) {
|
||||||
// Initialize the Map on first usage.
|
// Initialize the Map on first usage.
|
||||||
itemsRef.current = new Map()
|
itemsRef.current = new Map();
|
||||||
}
|
}
|
||||||
return itemsRef.current
|
return itemsRef.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNode(index: number) {
|
function getNode(index: number) {
|
||||||
const map = getMap()
|
const map = getMap();
|
||||||
const node = map?.get(index)
|
const node = map?.get(index);
|
||||||
return node
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusInput(itemId: number) {
|
function focusInput(itemId: number) {
|
||||||
const node = getNode(itemId)
|
const node = getNode(itemId);
|
||||||
if (node) {
|
if (node) {
|
||||||
node.focus()
|
node.focus();
|
||||||
node.placeholder = ''
|
node.placeholder = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFocus(
|
function handleFocus(
|
||||||
event: React.FocusEvent<HTMLInputElement>,
|
event: React.FocusEvent<HTMLInputElement>,
|
||||||
index: number
|
index: number
|
||||||
) {
|
) {
|
||||||
event.target.select()
|
event.target.select();
|
||||||
focusInput(index)
|
focusInput(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBlur(index: number) {
|
function handleBlur(index: number) {
|
||||||
const node = getNode(index)
|
const node = getNode(index);
|
||||||
if (node) {
|
if (node) {
|
||||||
node.placeholder = placeholder
|
node.placeholder = placeholder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateInputField(val: string, index: number) {
|
function updateInputField(val: string, index: number) {
|
||||||
const node = getNode(index)
|
const node = getNode(index);
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
node.value = val
|
node.value = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPins((prev) =>
|
setPins((prev) =>
|
||||||
prev.map((p, i) => {
|
prev.map((p, i) => {
|
||||||
if (i === index) {
|
if (i === index) {
|
||||||
return val
|
return val;
|
||||||
} else {
|
} else {
|
||||||
return p
|
return p;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(value: string) {
|
function validate(value: string) {
|
||||||
const NUMERIC_REGEX = /^[0-9]+$/
|
const NUMERIC_REGEX = /^[0-9]+$/;
|
||||||
const ALPHA_NUMERIC_REGEX = /^[a-zA-Z0-9]+$/i
|
const ALPHA_NUMERIC_REGEX = /^[a-zA-Z0-9]+$/i;
|
||||||
const regex = type === 'alphanumeric' ? ALPHA_NUMERIC_REGEX : NUMERIC_REGEX
|
const regex = type === "alphanumeric" ? ALPHA_NUMERIC_REGEX : NUMERIC_REGEX;
|
||||||
return regex.test(value)
|
return regex.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pastedVal = React.useRef<null | string>(null)
|
const pastedVal = React.useRef<null | string>(null);
|
||||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>, index: number) {
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>, index: number) {
|
||||||
const inputValue = e.target.value
|
const inputValue = e.target.value;
|
||||||
const pastedValue = pastedVal.current
|
const pastedValue = pastedVal.current;
|
||||||
const inputChar =
|
const inputChar =
|
||||||
pastedValue && pastedValue.length === length
|
pastedValue && pastedValue.length === length
|
||||||
? pastedValue.charAt(length - 1)
|
? pastedValue.charAt(length - 1)
|
||||||
: inputValue.slice(-1)
|
: inputValue.slice(-1);
|
||||||
|
|
||||||
if (validate(inputChar)) {
|
if (validate(inputChar)) {
|
||||||
updateInputField(inputChar, index)
|
updateInputField(inputChar, index);
|
||||||
pastedVal.current = null
|
pastedVal.current = null;
|
||||||
if (inputValue.length > 0) {
|
if (inputValue.length > 0) {
|
||||||
focusInput(index + 1)
|
focusInput(index + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
|
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
const copyValue = event.clipboardData
|
const copyValue = event.clipboardData
|
||||||
.getData('text/plain')
|
.getData("text/plain")
|
||||||
.replace(/[\n\r\s]+/g, '')
|
.replace(/[\n\r\s]+/g, "");
|
||||||
const copyArr = copyValue.split('').slice(0, length)
|
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++) {
|
for (let i = 0; i < length; i++) {
|
||||||
if (i < copyArr.length) {
|
if (i < copyArr.length) {
|
||||||
updateInputField(copyArr[i], i)
|
updateInputField(copyArr[i], i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pastedVal.current = copyValue
|
pastedVal.current = copyValue;
|
||||||
focusInput(copyArr.length < length ? copyArr.length : length - 1)
|
focusInput(copyArr.length < length ? copyArr.length : length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(
|
function handleKeyDown(
|
||||||
event: React.KeyboardEvent<HTMLInputElement>,
|
event: React.KeyboardEvent<HTMLInputElement>,
|
||||||
index: number
|
index: number
|
||||||
) {
|
) {
|
||||||
const { ctrlKey, key, shiftKey, metaKey } = event
|
const { ctrlKey, key, shiftKey, metaKey } = event;
|
||||||
|
|
||||||
if (type === 'numeric') {
|
if (type === "numeric") {
|
||||||
const canTypeSign =
|
const canTypeSign =
|
||||||
key === 'Backspace' ||
|
key === "Backspace" ||
|
||||||
key === 'Tab' ||
|
key === "Tab" ||
|
||||||
key === 'Control' ||
|
key === "Control" ||
|
||||||
key === 'Delete' ||
|
key === "Delete" ||
|
||||||
(ctrlKey && key === 'v') ||
|
(ctrlKey && key === "v") ||
|
||||||
(metaKey && key === 'v')
|
(metaKey && key === "v")
|
||||||
? true
|
? true
|
||||||
: !Number.isNaN(Number(key))
|
: !Number.isNaN(Number(key));
|
||||||
|
|
||||||
if (!canTypeSign || readOnly) {
|
if (!canTypeSign || readOnly) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'ArrowLeft' || (shiftKey && key === 'Tab')) {
|
if (key === "ArrowLeft" || (shiftKey && key === "Tab")) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
focusInput(index - 1)
|
focusInput(index - 1);
|
||||||
} else if (key === 'ArrowRight' || key === 'Tab' || key === ' ') {
|
} else if (key === "ArrowRight" || key === "Tab" || key === " ") {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
focusInput(index + 1)
|
focusInput(index + 1);
|
||||||
} else if (key === 'Delete') {
|
} else if (key === "Delete") {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
} else if (key === 'Backspace') {
|
} else if (key === "Backspace") {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
updateInputField('', index)
|
updateInputField("", index);
|
||||||
if ((event.target as HTMLInputElement).value === '') {
|
if ((event.target as HTMLInputElement).value === "") {
|
||||||
focusInput(index - 1)
|
focusInput(index - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pins,
|
pins,
|
||||||
pinValue,
|
pinValue,
|
||||||
refMap: getMap(),
|
refMap: getMap(),
|
||||||
handleFocus,
|
handleFocus,
|
||||||
handleBlur,
|
handleBlur,
|
||||||
handleChange,
|
handleChange,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
handleKeyDown,
|
handleKeyDown
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
/* ========== Util Func ========== */
|
/* ========== Util Func ========== */
|
||||||
|
|
||||||
const getValidChildren = (children: React.ReactNode) =>
|
const getValidChildren = (children: React.ReactNode) =>
|
||||||
React.Children.toArray(children).filter((child) => {
|
React.Children.toArray(children).filter((child) => {
|
||||||
if (React.isValidElement(child)) {
|
if (React.isValidElement(child)) {
|
||||||
return React.isValidElement(child)
|
return React.isValidElement(child);
|
||||||
}
|
}
|
||||||
throw new Error(`${PinInput.displayName} contains invalid children.`)
|
throw new Error(`${PinInput.displayName} contains invalid children.`);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
}) as React.ReactElement<any>[]
|
}) as React.ReactElement<any>[];
|
||||||
|
|
||||||
const getInputFieldCount = (children: React.ReactNode) =>
|
const getInputFieldCount = (children: React.ReactNode) =>
|
||||||
React.Children.toArray(children).filter((child) => {
|
React.Children.toArray(children).filter((child) => {
|
||||||
if (React.isValidElement(child) && child.type === PinInputField) {
|
if (React.isValidElement(child) && child.type === PinInputField) {
|
||||||
return React.isValidElement(child)
|
return React.isValidElement(child);
|
||||||
}
|
}
|
||||||
}).length
|
}).length;
|
||||||
|
|
||||||
export { PinInput, PinInputField }
|
export { PinInput, PinInputField };
|
||||||
@@ -1,65 +1,65 @@
|
|||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from "@tanstack/react-router";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
export function ProfileDropdown() {
|
export function ProfileDropdown() {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
|
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||||
<Avatar className='h-8 w-8'>
|
<Avatar className="h-8 w-8">
|
||||||
<AvatarImage src='/avatars/01.png' alt='@shadcn' />
|
<AvatarImage src="/avatars/01.png" alt="@shadcn" />
|
||||||
<AvatarFallback>SN</AvatarFallback>
|
<AvatarFallback>SN</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className='w-56' align='end' forceMount>
|
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||||
<DropdownMenuLabel className='font-normal'>
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className='flex flex-col space-y-1'>
|
<div className="flex flex-col space-y-1">
|
||||||
<p className='text-sm font-medium leading-none'>satnaing</p>
|
<p className="text-sm font-medium leading-none">satnaing</p>
|
||||||
<p className='text-xs leading-none text-muted-foreground'>
|
<p className="text-xs leading-none text-muted-foreground">
|
||||||
satnaingdev@gmail.com
|
satnaingdev@gmail.com
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to='/settings'>
|
<Link to="/settings">
|
||||||
Profile
|
Profile
|
||||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to='/settings'>
|
<Link to="/settings">
|
||||||
Billing
|
Billing
|
||||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to='/settings'>
|
<Link to="/settings">
|
||||||
Settings
|
Settings
|
||||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
Log out
|
Log out
|
||||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,32 @@
|
|||||||
import { IconSearch } from '@tabler/icons-react'
|
import { IconSearch } from "@tabler/icons-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { useSearch } from '@/context/search-context'
|
import { useSearch } from "@/context/search-context";
|
||||||
import { Button } from './ui/button'
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string;
|
||||||
type?: React.HTMLInputTypeAttribute
|
type?: React.HTMLInputTypeAttribute;
|
||||||
placeholder?: string
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Search({ className = '', placeholder = 'Search' }: Props) {
|
export function Search({ className = "", placeholder = "Search" }: Props) {
|
||||||
const { setOpen } = useSearch()
|
const { setOpen } = useSearch();
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative h-8 w-full flex-1 justify-start rounded-md bg-muted/25 text-sm font-normal text-muted-foreground shadow-none hover:bg-muted/50 sm:pr-12 md:w-40 md:flex-none lg:w-56 xl:w-64',
|
"relative h-8 w-full flex-1 justify-start rounded-md bg-muted/25 text-sm font-normal text-muted-foreground shadow-none hover:bg-muted/50 sm:pr-12 md:w-40 md:flex-none lg:w-56 xl:w-64",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}>
|
||||||
>
|
<IconSearch
|
||||||
<IconSearch
|
aria-hidden="true"
|
||||||
aria-hidden='true'
|
className="absolute left-1.5 top-1/2 -translate-y-1/2"
|
||||||
className='absolute left-1.5 top-1/2 -translate-y-1/2'
|
/>
|
||||||
/>
|
<span className="ml-3">{placeholder}</span>
|
||||||
<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">
|
||||||
<kbd className='pointer-events-none absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex'>
|
<span className="text-xs">⌘</span>K
|
||||||
<span className='text-xs'>⌘</span>K
|
</kbd>
|
||||||
</kbd>
|
</Button>
|
||||||
</Button>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,62 +1,62 @@
|
|||||||
import { IconLoader } from '@tabler/icons-react'
|
import { IconLoader } from "@tabler/icons-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { FormControl } from '@/components/ui/form'
|
import { FormControl } from "@/components/ui/form";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue
|
||||||
} from '@/components/ui/select'
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
interface SelectDropdownProps {
|
interface SelectDropdownProps {
|
||||||
onValueChange?: (value: string) => void
|
onValueChange?: (value: string) => void;
|
||||||
defaultValue: string | undefined
|
defaultValue: string | undefined;
|
||||||
placeholder?: string
|
placeholder?: string;
|
||||||
isPending?: boolean
|
isPending?: boolean;
|
||||||
items: { label: string; value: string }[] | undefined
|
items: { label: string; value: string }[] | undefined;
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
className?: string
|
className?: string;
|
||||||
isControlled?: boolean
|
isControlled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectDropdown({
|
export function SelectDropdown({
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
isPending,
|
isPending,
|
||||||
items,
|
items,
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
className = '',
|
className = "",
|
||||||
isControlled = false,
|
isControlled = false
|
||||||
}: SelectDropdownProps) {
|
}: SelectDropdownProps) {
|
||||||
const defaultState = isControlled
|
const defaultState = isControlled
|
||||||
? { value: defaultValue, onValueChange }
|
? { value: defaultValue, onValueChange }
|
||||||
: { defaultValue, onValueChange }
|
: { defaultValue, onValueChange };
|
||||||
return (
|
return (
|
||||||
<Select {...defaultState}>
|
<Select {...defaultState}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger disabled={disabled} className={cn(className)}>
|
<SelectTrigger disabled={disabled} className={cn(className)}>
|
||||||
<SelectValue placeholder={placeholder ?? 'Select'} />
|
<SelectValue placeholder={placeholder ?? "Select"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<SelectItem disabled value='loading' className='h-14'>
|
<SelectItem disabled value="loading" className="h-14">
|
||||||
<div className='flex items-center justify-center gap-2'>
|
<div className="flex items-center justify-center gap-2">
|
||||||
<IconLoader className='h-5 w-5 animate-spin' />
|
<IconLoader className="h-5 w-5 animate-spin" />
|
||||||
{' '}
|
{" "}
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
) : (
|
) : (
|
||||||
items?.map(({ label, value }) => (
|
items?.map(({ label, value }) => (
|
||||||
<SelectItem key={value} value={value}>
|
<SelectItem key={value} value={value}>
|
||||||
{label}
|
{label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
const SkipToMain = () => {
|
const SkipToMain = () => {
|
||||||
return (
|
return (
|
||||||
<a
|
<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`}
|
className={`fixed left-44 z-[999] -translate-y-52 whitespace-nowrap bg-primary px-4 py-2 text-sm font-medium text-primary-foreground opacity-95 shadow transition hover:bg-primary/90 focus:translate-y-3 focus:transform focus-visible:ring-1 focus-visible:ring-ring`}
|
||||||
href='#content'
|
href="#content">
|
||||||
>
|
Skip to Main
|
||||||
Skip to Main
|
</a>
|
||||||
</a>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default SkipToMain
|
export default SkipToMain;
|
||||||
@@ -1,89 +1,86 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { ChevronsUpDown, Plus } from "lucide-react"
|
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function TeamSwitcher({
|
export function TeamSwitcher({
|
||||||
teams,
|
teams
|
||||||
}: {
|
}: {
|
||||||
teams: {
|
teams: {
|
||||||
name: string
|
name: string;
|
||||||
logo: React.ElementType
|
logo: React.ElementType;
|
||||||
plan: string
|
plan: string;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
const [activeTeam, setActiveTeam] = React.useState(teams[0])
|
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size="lg"
|
size="lg"
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||||
>
|
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||||
<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" />
|
||||||
<activeTeam.logo className="size-4" />
|
</div>
|
||||||
</div>
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<span className="truncate font-semibold">
|
||||||
<span className="truncate font-semibold">
|
{activeTeam.name}
|
||||||
{activeTeam.name}
|
</span>
|
||||||
</span>
|
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
</div>
|
||||||
</div>
|
<ChevronsUpDown className="ml-auto" />
|
||||||
<ChevronsUpDown className="ml-auto" />
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent
|
||||||
<DropdownMenuContent
|
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
align="start"
|
||||||
align="start"
|
side={isMobile ? "bottom" : "right"}
|
||||||
side={isMobile ? "bottom" : "right"}
|
sideOffset={4}>
|
||||||
sideOffset={4}
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
>
|
Teams
|
||||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
</DropdownMenuLabel>
|
||||||
Teams
|
{teams.map((team, index) => (
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuItem
|
||||||
{teams.map((team, index) => (
|
key={team.name}
|
||||||
<DropdownMenuItem
|
onClick={() => setActiveTeam(team)}
|
||||||
key={team.name}
|
className="gap-2 p-2">
|
||||||
onClick={() => setActiveTeam(team)}
|
<div className="flex size-6 items-center justify-center rounded-sm border">
|
||||||
className="gap-2 p-2"
|
<team.logo className="size-4 shrink-0" />
|
||||||
>
|
</div>
|
||||||
<div className="flex size-6 items-center justify-center rounded-sm border">
|
{team.name}
|
||||||
<team.logo className="size-4 shrink-0" />
|
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
{team.name}
|
))}
|
||||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem className="gap-2 p-2">
|
||||||
))}
|
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
||||||
<DropdownMenuSeparator />
|
<Plus className="size-4" />
|
||||||
<DropdownMenuItem className="gap-2 p-2">
|
</div>
|
||||||
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
<div className="font-medium text-muted-foreground">Add team</div>
|
||||||
<Plus className="size-4" />
|
</DropdownMenuItem>
|
||||||
</div>
|
</DropdownMenuContent>
|
||||||
<div className="font-medium text-muted-foreground">Add team</div>
|
</DropdownMenu>
|
||||||
</DropdownMenuItem>
|
</SidebarMenuItem>
|
||||||
</DropdownMenuContent>
|
</SidebarMenu>
|
||||||
</DropdownMenu>
|
);
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,58 +1,58 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from "react";
|
||||||
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react'
|
import { IconCheck, IconMoon, IconSun } from "@tabler/icons-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { useTheme } from '@/context/theme-context'
|
import { useTheme } from "@/context/theme-context";
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
export function ThemeSwitch() {
|
export function ThemeSwitch() {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
/* Update theme-color meta tag
|
/* Update theme-color meta tag
|
||||||
* when theme is updated */
|
* when theme is updated */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const themeColor = theme === 'dark' ? '#020817' : '#fff'
|
const themeColor = theme === "dark" ? "#020817" : "#fff";
|
||||||
const metaThemeColor = document.querySelector("meta[name='theme-color']")
|
const metaThemeColor = document.querySelector("meta[name='theme-color']");
|
||||||
if (metaThemeColor) metaThemeColor.setAttribute('content', themeColor)
|
if (metaThemeColor) metaThemeColor.setAttribute("content", themeColor);
|
||||||
}, [theme])
|
}, [theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant='ghost' size='icon' className='scale-95 rounded-full'>
|
<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' />
|
<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' />
|
<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>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end'>
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||||
Light{' '}
|
Light{" "}
|
||||||
<IconCheck
|
<IconCheck
|
||||||
size={14}
|
size={14}
|
||||||
className={cn('ml-auto', theme !== 'light' && 'hidden')}
|
className={cn("ml-auto", theme !== "light" && "hidden")}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||||
Dark
|
Dark
|
||||||
<IconCheck
|
<IconCheck
|
||||||
size={14}
|
size={14}
|
||||||
className={cn('ml-auto', theme !== 'dark' && 'hidden')}
|
className={cn("ml-auto", theme !== "dark" && "hidden")}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
System
|
System
|
||||||
<IconCheck
|
<IconCheck
|
||||||
size={14}
|
size={14}
|
||||||
className={cn('ml-auto', theme !== 'system' && 'hidden')}
|
className={cn("ml-auto", theme !== "system" && "hidden")}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,138 +1,138 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
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<
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
const AlertDialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AlertDialogPortal>
|
<AlertDialogPortal>
|
||||||
<AlertDialogOverlay />
|
<AlertDialogOverlay />
|
||||||
<AlertDialogPrimitive.Content
|
<AlertDialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
</AlertDialogPortal>
|
||||||
))
|
));
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
const AlertDialogHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col space-y-2 text-center sm:text-left',
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
AlertDialogHeader.displayName = 'AlertDialogHeader'
|
AlertDialogHeader.displayName = "AlertDialogHeader";
|
||||||
|
|
||||||
const AlertDialogFooter = ({
|
const AlertDialogFooter = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
AlertDialogFooter.displayName = 'AlertDialogFooter'
|
AlertDialogFooter.displayName = "AlertDialogFooter";
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
const AlertDialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AlertDialogPrimitive.Title
|
<AlertDialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-lg font-semibold', className)}
|
className={cn("text-lg font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
const AlertDialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm text-muted-foreground', className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertDialogDescription.displayName =
|
AlertDialogDescription.displayName =
|
||||||
AlertDialogPrimitive.Description.displayName
|
AlertDialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
const AlertDialogAction = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AlertDialogPrimitive.Action
|
<AlertDialogPrimitive.Action
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(buttonVariants(), className)}
|
className={cn(buttonVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
const AlertDialogCancel = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AlertDialogPrimitive.Cancel
|
<AlertDialogPrimitive.Cancel
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: 'outline' }),
|
buttonVariants({ variant: "outline" }),
|
||||||
'mt-2 sm:mt-0',
|
"mt-2 sm:mt-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogPortal,
|
AlertDialogPortal,
|
||||||
AlertDialogOverlay,
|
AlertDialogOverlay,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel
|
||||||
}
|
};
|
||||||
@@ -1,58 +1,58 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
|
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-background text-foreground',
|
default: "bg-background text-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: "default"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
const Alert = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
>(({ className, variant, ...props }, ref) => (
|
>(({ className, variant, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
role='alert'
|
role="alert"
|
||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Alert.displayName = 'Alert'
|
Alert.displayName = "Alert";
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
const AlertTitle = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<h5
|
<h5
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertTitle.displayName = 'AlertTitle'
|
AlertTitle.displayName = "AlertTitle";
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
const AlertDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertDescription.displayName = 'AlertDescription'
|
AlertDescription.displayName = "AlertDescription";
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
@@ -1,47 +1,47 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
const Avatar = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
const AvatarImage = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AvatarPrimitive.Image
|
<AvatarPrimitive.Image
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('aspect-square h-full w-full', className)}
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
const AvatarFallback = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-full w-full items-center justify-center rounded-full bg-muted',
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||||
secondary:
|
secondary:
|
||||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
destructive:
|
destructive:
|
||||||
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||||
outline: 'text-foreground',
|
outline: "text-foreground"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: "default"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
VariantProps<typeof badgeVariants> {}
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge, badgeVariants };
|
||||||
@@ -1,115 +1,113 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Breadcrumb = React.forwardRef<
|
const Breadcrumb = React.forwardRef<
|
||||||
HTMLElement,
|
HTMLElement,
|
||||||
React.ComponentPropsWithoutRef<"nav"> & {
|
React.ComponentPropsWithoutRef<"nav"> & {
|
||||||
separator?: React.ReactNode
|
separator?: React.ReactNode;
|
||||||
}
|
}
|
||||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
||||||
Breadcrumb.displayName = "Breadcrumb"
|
Breadcrumb.displayName = "Breadcrumb";
|
||||||
|
|
||||||
const BreadcrumbList = React.forwardRef<
|
const BreadcrumbList = React.forwardRef<
|
||||||
HTMLOListElement,
|
HTMLOListElement,
|
||||||
React.ComponentPropsWithoutRef<"ol">
|
React.ComponentPropsWithoutRef<"ol">
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ol
|
<ol
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
BreadcrumbList.displayName = "BreadcrumbList"
|
BreadcrumbList.displayName = "BreadcrumbList";
|
||||||
|
|
||||||
const BreadcrumbItem = React.forwardRef<
|
const BreadcrumbItem = React.forwardRef<
|
||||||
HTMLLIElement,
|
HTMLLIElement,
|
||||||
React.ComponentPropsWithoutRef<"li">
|
React.ComponentPropsWithoutRef<"li">
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<li
|
<li
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||||
|
|
||||||
const BreadcrumbLink = React.forwardRef<
|
const BreadcrumbLink = React.forwardRef<
|
||||||
HTMLAnchorElement,
|
HTMLAnchorElement,
|
||||||
React.ComponentPropsWithoutRef<"a"> & {
|
React.ComponentPropsWithoutRef<"a"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
>(({ asChild, className, ...props }, ref) => {
|
>(({ asChild, className, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("transition-colors hover:text-foreground", className)}
|
className={cn("transition-colors hover:text-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||||
|
|
||||||
const BreadcrumbPage = React.forwardRef<
|
const BreadcrumbPage = React.forwardRef<
|
||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
React.ComponentPropsWithoutRef<"span">
|
React.ComponentPropsWithoutRef<"span">
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<span
|
<span
|
||||||
ref={ref}
|
ref={ref}
|
||||||
role="link"
|
role="link"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-current="page"
|
aria-current="page"
|
||||||
className={cn("font-normal text-foreground", className)}
|
className={cn("font-normal text-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||||
|
|
||||||
const BreadcrumbSeparator = ({
|
const BreadcrumbSeparator = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"li">) => (
|
}: React.ComponentProps<"li">) => (
|
||||||
<li
|
<li
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
{children ?? <ChevronRight />}
|
||||||
{children ?? <ChevronRight />}
|
</li>
|
||||||
</li>
|
);
|
||||||
)
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
|
||||||
|
|
||||||
const BreadcrumbEllipsis = ({
|
const BreadcrumbEllipsis = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span">) => (
|
}: React.ComponentProps<"span">) => (
|
||||||
<span
|
<span
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<span className="sr-only">More</span>
|
||||||
<span className="sr-only">More</span>
|
</span>
|
||||||
</span>
|
);
|
||||||
)
|
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
||||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
BreadcrumbEllipsis,
|
BreadcrumbEllipsis
|
||||||
}
|
};
|
||||||
@@ -1,56 +1,56 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { Slot } from '@radix-ui/react-slot'
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
secondary:
|
secondary:
|
||||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: "text-primary underline-offset-4 hover:underline"
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-9 px-4 py-2',
|
default: "h-9 px-4 py-2",
|
||||||
sm: 'h-8 rounded-md px-3 text-xs',
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
lg: 'h-10 rounded-md px-8',
|
lg: "h-10 rounded-md px-8",
|
||||||
icon: 'h-9 w-9',
|
icon: "h-9 w-9"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: "default",
|
||||||
size: 'default',
|
size: "default"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : 'button'
|
const Comp = asChild ? Slot : "button";
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Button.displayName = 'Button'
|
Button.displayName = "Button";
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
@@ -1,69 +1,69 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { DayPicker } from 'react-day-picker'
|
import { DayPicker } from "react-day-picker";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
classNames,
|
classNames,
|
||||||
showOutsideDays = true,
|
showOutsideDays = true,
|
||||||
...props
|
...props
|
||||||
}: CalendarProps) {
|
}: CalendarProps) {
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
showOutsideDays={showOutsideDays}
|
showOutsideDays={showOutsideDays}
|
||||||
className={cn('p-3', className)}
|
className={cn("p-3", className)}
|
||||||
classNames={{
|
classNames={{
|
||||||
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
month: 'space-y-4',
|
month: "space-y-4",
|
||||||
caption: 'flex justify-center pt-1 relative items-center',
|
caption: "flex justify-center pt-1 relative items-center",
|
||||||
caption_label: 'text-sm font-medium',
|
caption_label: "text-sm font-medium",
|
||||||
nav: 'space-x-1 flex items-center',
|
nav: "space-x-1 flex items-center",
|
||||||
nav_button: cn(
|
nav_button: cn(
|
||||||
buttonVariants({ variant: 'outline' }),
|
buttonVariants({ variant: "outline" }),
|
||||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||||
),
|
),
|
||||||
nav_button_previous: 'absolute left-1',
|
nav_button_previous: "absolute left-1",
|
||||||
nav_button_next: 'absolute right-1',
|
nav_button_next: "absolute right-1",
|
||||||
table: 'w-full border-collapse space-y-1',
|
table: "w-full border-collapse space-y-1",
|
||||||
head_row: 'flex',
|
head_row: "flex",
|
||||||
head_cell:
|
head_cell:
|
||||||
'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
|
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||||
row: 'flex w-full mt-2',
|
row: "flex w-full mt-2",
|
||||||
cell: cn(
|
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',
|
"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'
|
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(>.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'
|
: "[&:has([aria-selected])]:rounded-md"
|
||||||
),
|
),
|
||||||
day: cn(
|
day: cn(
|
||||||
buttonVariants({ variant: 'ghost' }),
|
buttonVariants({ variant: "ghost" }),
|
||||||
'h-8 w-8 p-0 font-normal aria-selected:opacity-100'
|
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
||||||
),
|
),
|
||||||
day_range_start: 'day-range-start',
|
day_range_start: "day-range-start",
|
||||||
day_range_end: 'day-range-end',
|
day_range_end: "day-range-end",
|
||||||
day_selected:
|
day_selected:
|
||||||
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
day_today: 'bg-accent text-accent-foreground',
|
day_today: "bg-accent text-accent-foreground",
|
||||||
day_outside:
|
day_outside:
|
||||||
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
|
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||||
day_disabled: 'text-muted-foreground opacity-50',
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
day_range_middle:
|
day_range_middle:
|
||||||
'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
day_hidden: 'invisible',
|
day_hidden: "invisible",
|
||||||
...classNames,
|
...classNames
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
IconLeft: () => <ChevronLeft className='h-4 w-4' />,
|
IconLeft: () => <ChevronLeft className="h-4 w-4" />,
|
||||||
IconRight: () => <ChevronRight className='h-4 w-4' />,
|
IconRight: () => <ChevronRight className="h-4 w-4" />
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
Calendar.displayName = 'Calendar'
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
export { Calendar }
|
export { Calendar };
|
||||||
@@ -1,75 +1,82 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
const Card = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-xl border bg-card text-card-foreground shadow',
|
"rounded-xl border bg-card text-card-foreground shadow",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Card.displayName = 'Card'
|
Card.displayName = "Card";
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
const CardHeader = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardHeader.displayName = 'CardHeader'
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
const CardTitle = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('font-semibold leading-none tracking-tight', className)}
|
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardTitle.displayName = 'CardTitle'
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
const CardDescription = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm text-muted-foreground', className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardDescription.displayName = 'CardDescription'
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
const CardContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
))
|
));
|
||||||
CardContent.displayName = 'CardContent'
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
const CardFooter = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('flex items-center p-6 pt-0', className)}
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardFooter.displayName = 'CardFooter'
|
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 React from "react";
|
||||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
import { Check } from 'lucide-react'
|
import { Check } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<CheckboxPrimitive.Indicator
|
||||||
<CheckboxPrimitive.Indicator
|
className={cn("flex items-center justify-center text-current")}>
|
||||||
className={cn('flex items-center justify-center text-current')}
|
<Check className="h-4 w-4" />
|
||||||
>
|
</CheckboxPrimitive.Indicator>
|
||||||
<Check className='h-4 w-4' />
|
</CheckboxPrimitive.Root>
|
||||||
</CheckboxPrimitive.Indicator>
|
));
|
||||||
</CheckboxPrimitive.Root>
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
))
|
|
||||||
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 * as React from "react";
|
||||||
import { type DialogProps } from '@radix-ui/react-dialog'
|
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||||
import { Command as CommandPrimitive } from 'cmdk'
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogTitle,
|
DialogTitle
|
||||||
} from '@/components/ui/dialog'
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
const Command = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CommandPrimitive
|
<CommandPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Command.displayName = CommandPrimitive.displayName
|
Command.displayName = CommandPrimitive.displayName;
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className='overflow-hidden p-0'>
|
<DialogContent className="overflow-hidden p-0">
|
||||||
<VisuallyHidden asChild>
|
<VisuallyHidden asChild>
|
||||||
<DialogTitle />
|
<DialogTitle />
|
||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
<VisuallyHidden asChild>
|
<VisuallyHidden asChild>
|
||||||
<DialogDescription />
|
<DialogDescription />
|
||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
<Command className='[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'>
|
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
const CommandInput = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
|
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||||
<Search className='mr-2 h-4 w-4 shrink-0 opacity-50' />
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
const CommandList = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CommandPrimitive.List
|
<CommandPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName
|
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
const CommandEmpty = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
>((props, ref) => (
|
>((props, ref) => (
|
||||||
<CommandPrimitive.Empty
|
<CommandPrimitive.Empty
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className='py-6 text-center text-sm'
|
className="py-6 text-center text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
const CommandGroup = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CommandPrimitive.Group
|
<CommandPrimitive.Group
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
const CommandSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CommandPrimitive.Separator
|
<CommandPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('-mx-1 h-px bg-border', className)}
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
const CommandItem = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||||
|
|
||||||
const CommandShortcut = ({
|
const CommandShortcut = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-auto text-xs tracking-widest text-muted-foreground',
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
CommandShortcut.displayName = 'CommandShortcut'
|
CommandShortcut.displayName = "CommandShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
CommandSeparator,
|
CommandSeparator
|
||||||
}
|
};
|
||||||
@@ -1,121 +1,120 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { X } from 'lucide-react'
|
import { X } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils'
|
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<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
{children}
|
||||||
{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">
|
||||||
<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" />
|
||||||
<X className='h-4 w-4' />
|
<span className="sr-only">Close</span>
|
||||||
<span className='sr-only'>Close</span>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Content>
|
||||||
</DialogPrimitive.Content>
|
</DialogPortal>
|
||||||
</DialogPortal>
|
));
|
||||||
))
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
DialogHeader.displayName = 'DialogHeader'
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
DialogFooter.displayName = 'DialogFooter'
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-lg font-semibold leading-none tracking-tight',
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm text-muted-foreground', className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription
|
||||||
}
|
};
|
||||||
@@ -1,198 +1,195 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import { Check, ChevronRight, Circle } from 'lucide-react'
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils'
|
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<
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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',
|
"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',
|
inset && "pl-8",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
{children}
|
||||||
{children}
|
<ChevronRight className="ml-auto" />
|
||||||
<ChevronRight className='ml-auto' />
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
));
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
DropdownMenuSubTrigger.displayName =
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSubContent.displayName =
|
DropdownMenuSubContent.displayName =
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
"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',
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
));
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
const DropdownMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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',
|
"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',
|
inset && "pl-8",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<Check className="h-4 w-4" />
|
||||||
<Check className='h-4 w-4' />
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</span>
|
||||||
</span>
|
{children}
|
||||||
{children}
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
));
|
||||||
))
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
DropdownMenuCheckboxItem.displayName =
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
<Circle className='h-2 w-2 fill-current' />
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</span>
|
||||||
</span>
|
{children}
|
||||||
{children}
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
));
|
||||||
))
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1.5 text-sm font-semibold',
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
inset && 'pl-8',
|
inset && "pl-8",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
const DropdownMenuShortcut = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup
|
||||||
}
|
};
|
||||||
+131
-131
@@ -1,175 +1,175 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
ControllerProps,
|
ControllerProps,
|
||||||
FieldPath,
|
FieldPath,
|
||||||
FieldValues,
|
FieldValues,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
useFormContext,
|
useFormContext
|
||||||
} from 'react-hook-form'
|
} from "react-hook-form";
|
||||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import { Slot } from '@radix-ui/react-slot'
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
const Form = FormProvider
|
const Form = FormProvider;
|
||||||
|
|
||||||
type FormFieldContextValue<
|
type FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
> = {
|
> = {
|
||||||
name: TName
|
name: TName;
|
||||||
}
|
};
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
{} as FormFieldContextValue
|
{} as FormFieldContextValue
|
||||||
)
|
);
|
||||||
|
|
||||||
const FormField = <
|
const FormField = <
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
>({
|
>({
|
||||||
...props
|
...props
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
<Controller {...props} />
|
<Controller {...props} />
|
||||||
</FormFieldContext.Provider>
|
</FormFieldContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const useFormField = () => {
|
const useFormField = () => {
|
||||||
const fieldContext = React.useContext(FormFieldContext)
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
const itemContext = React.useContext(FormItemContext)
|
const itemContext = React.useContext(FormItemContext);
|
||||||
const { getFieldState, formState } = useFormContext()
|
const { getFieldState, formState } = useFormContext();
|
||||||
|
|
||||||
const fieldState = getFieldState(fieldContext.name, formState)
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
if (!fieldContext) {
|
if (!fieldContext) {
|
||||||
throw new Error('useFormField should be used within <FormField>')
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = itemContext
|
const { id } = itemContext;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: fieldContext.name,
|
name: fieldContext.name,
|
||||||
formItemId: `${id}-form-item`,
|
formItemId: `${id}-form-item`,
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
formMessageId: `${id}-form-item-message`,
|
formMessageId: `${id}-form-item-message`,
|
||||||
...fieldState,
|
...fieldState
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type FormItemContextValue = {
|
type FormItemContextValue = {
|
||||||
id: string
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
{} as FormItemContextValue
|
{} as FormItemContextValue
|
||||||
)
|
);
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
const FormItem = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const id = React.useId()
|
const id = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItemContext.Provider value={{ id }}>
|
<FormItemContext.Provider value={{ id }}>
|
||||||
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormItem.displayName = 'FormItem'
|
FormItem.displayName = "FormItem";
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
const FormLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { error, formItemId } = useFormField()
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(error && 'text-destructive', className)}
|
className={cn(error && "text-destructive", className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormLabel.displayName = 'FormLabel'
|
FormLabel.displayName = "FormLabel";
|
||||||
|
|
||||||
const FormControl = React.forwardRef<
|
const FormControl = React.forwardRef<
|
||||||
React.ElementRef<typeof Slot>,
|
React.ElementRef<typeof Slot>,
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
|
useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
!error
|
!error
|
||||||
? `${formDescriptionId}`
|
? `${formDescriptionId}`
|
||||||
: `${formDescriptionId} ${formMessageId}`
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
}
|
}
|
||||||
aria-invalid={!!error}
|
aria-invalid={!!error}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormControl.displayName = 'FormControl'
|
FormControl.displayName = "FormControl";
|
||||||
|
|
||||||
const FormDescription = React.forwardRef<
|
const FormDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { formDescriptionId } = useFormField()
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formDescriptionId}
|
id={formDescriptionId}
|
||||||
className={cn('text-[0.8rem] text-muted-foreground', className)}
|
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormDescription.displayName = 'FormDescription'
|
FormDescription.displayName = "FormDescription";
|
||||||
|
|
||||||
const FormMessage = React.forwardRef<
|
const FormMessage = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, children, ...props }, ref) => {
|
>(({ className, children, ...props }, ref) => {
|
||||||
const { error, formMessageId } = useFormField()
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message) : children
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formMessageId}
|
id={formMessageId}
|
||||||
className={cn('text-[0.8rem] font-medium text-destructive', className)}
|
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
{body}
|
||||||
{body}
|
</p>
|
||||||
</p>
|
);
|
||||||
)
|
});
|
||||||
})
|
FormMessage.displayName = "FormMessage";
|
||||||
FormMessage.displayName = 'FormMessage'
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormField,
|
FormField
|
||||||
}
|
};
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Input.displayName = 'Input'
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export { Input }
|
export { Input };
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const labelVariants = cva(
|
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<
|
const Label = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
VariantProps<typeof labelVariants>
|
VariantProps<typeof labelVariants>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(labelVariants(), className)}
|
className={cn(labelVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Label }
|
export { Label };
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
import { cn } from '@/lib/utils'
|
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<
|
const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
))
|
));
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
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 React from "react";
|
||||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
import { Circle } from 'lucide-react'
|
import { Circle } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const RadioGroup = React.forwardRef<
|
const RadioGroup = React.forwardRef<
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Root
|
<RadioGroupPrimitive.Root
|
||||||
className={cn('grid gap-2', className)}
|
className={cn("grid gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||||
|
|
||||||
const RadioGroupItem = React.forwardRef<
|
const RadioGroupItem = React.forwardRef<
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
<RadioGroupPrimitive.Indicator className='flex items-center justify-center'>
|
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||||
<Circle className='h-3.5 w-3.5 fill-primary' />
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Item>
|
||||||
</RadioGroupPrimitive.Item>
|
);
|
||||||
)
|
});
|
||||||
})
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
export { RadioGroup, RadioGroupItem };
|
||||||
@@ -1,55 +1,52 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ScrollAreaProps
|
interface ScrollAreaProps
|
||||||
extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
|
extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
|
||||||
orientation?: 'horizontal' | 'vertical'
|
orientation?: "horizontal" | "vertical";
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
const ScrollArea = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
ScrollAreaProps
|
ScrollAreaProps
|
||||||
>(({ className, children, orientation = 'vertical', ...props }, ref) => (
|
>(({ className, children, orientation = "vertical", ...props }, ref) => (
|
||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('relative overflow-hidden', className)}
|
className={cn("relative overflow-hidden", className)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<ScrollAreaPrimitive.Viewport
|
||||||
<ScrollAreaPrimitive.Viewport
|
className={cn(
|
||||||
className={cn(
|
"h-full w-full rounded-[inherit]",
|
||||||
'h-full w-full rounded-[inherit]',
|
orientation === "horizontal" && "!overflow-x-auto"
|
||||||
orientation === 'horizontal' && '!overflow-x-auto'
|
)}>
|
||||||
)}
|
{children}
|
||||||
>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
{children}
|
<ScrollBar orientation={orientation} />
|
||||||
</ScrollAreaPrimitive.Viewport>
|
<ScrollAreaPrimitive.Corner />
|
||||||
<ScrollBar orientation={orientation} />
|
</ScrollAreaPrimitive.Root>
|
||||||
<ScrollAreaPrimitive.Corner />
|
));
|
||||||
</ScrollAreaPrimitive.Root>
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||||
))
|
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
const ScrollBar = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
ref={ref}
|
ref={ref}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex touch-none select-none transition-colors',
|
"flex touch-none select-none transition-colors",
|
||||||
orientation === 'vertical' &&
|
orientation === "vertical" &&
|
||||||
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||||
orientation === 'horizontal' &&
|
orientation === "horizontal" &&
|
||||||
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className='relative flex-1 rounded-full bg-border' />
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
));
|
||||||
))
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar };
|
||||||
@@ -1,156 +1,150 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils'
|
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<
|
const SelectTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
{children}
|
||||||
{children}
|
<SelectPrimitive.Icon asChild>
|
||||||
<SelectPrimitive.Icon asChild>
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
<ChevronDown className='h-4 w-4 opacity-50' />
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Trigger>
|
||||||
</SelectPrimitive.Trigger>
|
));
|
||||||
))
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex cursor-default items-center justify-center py-1',
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<ChevronUp className="h-4 w-4" />
|
||||||
<ChevronUp className='h-4 w-4' />
|
</SelectPrimitive.ScrollUpButton>
|
||||||
</SelectPrimitive.ScrollUpButton>
|
));
|
||||||
))
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex cursor-default items-center justify-center py-1',
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<ChevronDown className="h-4 w-4" />
|
||||||
<ChevronDown className='h-4 w-4' />
|
</SelectPrimitive.ScrollDownButton>
|
||||||
</SelectPrimitive.ScrollDownButton>
|
));
|
||||||
))
|
|
||||||
SelectScrollDownButton.displayName =
|
SelectScrollDownButton.displayName =
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
const SelectContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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',
|
"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' &&
|
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',
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<SelectScrollUpButton />
|
||||||
<SelectScrollUpButton />
|
<SelectPrimitive.Viewport
|
||||||
<SelectPrimitive.Viewport
|
className={cn(
|
||||||
className={cn(
|
"p-1",
|
||||||
'p-1',
|
position === "popper" &&
|
||||||
position === 'popper' &&
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
|
)}>
|
||||||
)}
|
{children}
|
||||||
>
|
</SelectPrimitive.Viewport>
|
||||||
{children}
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Viewport>
|
</SelectPrimitive.Content>
|
||||||
<SelectScrollDownButton />
|
</SelectPrimitive.Portal>
|
||||||
</SelectPrimitive.Content>
|
));
|
||||||
</SelectPrimitive.Portal>
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
))
|
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
const SelectLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
const SelectItem = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<span className='absolute right-2 flex h-3.5 w-3.5 items-center justify-center'>
|
<SelectPrimitive.ItemIndicator>
|
||||||
<SelectPrimitive.ItemIndicator>
|
<Check className="h-4 w-4" />
|
||||||
<Check className='h-4 w-4' />
|
</SelectPrimitive.ItemIndicator>
|
||||||
</SelectPrimitive.ItemIndicator>
|
</span>
|
||||||
</span>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
</SelectPrimitive.Item>
|
||||||
</SelectPrimitive.Item>
|
));
|
||||||
))
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
const SelectSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectLabel,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectScrollUpButton,
|
SelectScrollUpButton,
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton
|
||||||
}
|
};
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Separator = React.forwardRef<
|
const Separator = React.forwardRef<
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ className, orientation = 'horizontal', decorative = true, ...props },
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
ref
|
ref
|
||||||
) => (
|
) => (
|
||||||
<SeparatorPrimitive.Root
|
<SeparatorPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 bg-border',
|
"shrink-0 bg-border",
|
||||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Separator }
|
export { Separator };
|
||||||
@@ -1,139 +1,138 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { X } from 'lucide-react'
|
import { X } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils'
|
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<
|
const SheetOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const sheetVariants = cva(
|
const sheetVariants = cva(
|
||||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
side: {
|
side: {
|
||||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||||
bottom:
|
bottom:
|
||||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-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',
|
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:
|
right:
|
||||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
side: 'right',
|
side: "right"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
interface SheetContentProps
|
interface SheetContentProps
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
VariantProps<typeof sheetVariants> {}
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<
|
const SheetContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
SheetContentProps
|
SheetContentProps
|
||||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
>(({ side = "right", className, children, ...props }, ref) => (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(sheetVariants({ side }), className)}
|
className={cn(sheetVariants({ side }), className)}
|
||||||
{...props}
|
{...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">
|
||||||
<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" />
|
||||||
<X className='h-4 w-4' />
|
<span className="sr-only">Close</span>
|
||||||
<span className='sr-only'>Close</span>
|
</SheetPrimitive.Close>
|
||||||
</SheetPrimitive.Close>
|
{children}
|
||||||
{children}
|
</SheetPrimitive.Content>
|
||||||
</SheetPrimitive.Content>
|
</SheetPortal>
|
||||||
</SheetPortal>
|
));
|
||||||
))
|
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SheetHeader = ({
|
const SheetHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col space-y-2 text-center sm:text-left',
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
SheetHeader.displayName = 'SheetHeader'
|
SheetHeader.displayName = "SheetHeader";
|
||||||
|
|
||||||
const SheetFooter = ({
|
const SheetFooter = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
SheetFooter.displayName = 'SheetFooter'
|
SheetFooter.displayName = "SheetFooter";
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
const SheetTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-lg font-semibold text-foreground', className)}
|
className={cn("text-lg font-semibold text-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef<
|
const SheetDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm text-muted-foreground', className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetPortal,
|
SheetPortal,
|
||||||
SheetOverlay,
|
SheetOverlay,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetFooter,
|
SheetFooter,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetDescription,
|
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({
|
function Skeleton({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('animate-pulse rounded-md bg-primary/10', className)}
|
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Skeleton }
|
export { Skeleton };
|
||||||
@@ -1,26 +1,25 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as SwitchPrimitives from '@radix-ui/react-switch'
|
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
const Switch = React.forwardRef<
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}>
|
||||||
>
|
<SwitchPrimitives.Thumb
|
||||||
<SwitchPrimitives.Thumb
|
className={cn(
|
||||||
className={cn(
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</SwitchPrimitives.Root>
|
||||||
</SwitchPrimitives.Root>
|
));
|
||||||
))
|
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
|
||||||
|
|
||||||
export { Switch }
|
export { Switch };
|
||||||
@@ -1,119 +1,119 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Table = React.forwardRef<
|
const Table = React.forwardRef<
|
||||||
HTMLTableElement,
|
HTMLTableElement,
|
||||||
React.HTMLAttributes<HTMLTableElement>
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div className='relative w-full overflow-auto'>
|
<div className="relative w-full overflow-auto">
|
||||||
<table
|
<table
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('w-full caption-bottom text-sm', className)}
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
));
|
||||||
Table.displayName = 'Table'
|
Table.displayName = "Table";
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<
|
const TableHeader = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
))
|
));
|
||||||
TableHeader.displayName = 'TableHeader'
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
const TableBody = React.forwardRef<
|
const TableBody = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tbody
|
<tbody
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('[&_tr:last-child]:border-0', className)}
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableBody.displayName = 'TableBody'
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<
|
const TableFooter = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tfoot
|
<tfoot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableFooter.displayName = 'TableFooter'
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
const TableRow = React.forwardRef<
|
const TableRow = React.forwardRef<
|
||||||
HTMLTableRowElement,
|
HTMLTableRowElement,
|
||||||
React.HTMLAttributes<HTMLTableRowElement>
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableRow.displayName = 'TableRow'
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
const TableHead = React.forwardRef<
|
const TableHead = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<th
|
<th
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableHead.displayName = 'TableHead'
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
const TableCell = React.forwardRef<
|
const TableCell = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<td
|
<td
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableCell.displayName = 'TableCell'
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<
|
const TableCaption = React.forwardRef<
|
||||||
HTMLTableCaptionElement,
|
HTMLTableCaptionElement,
|
||||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<caption
|
<caption
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableCaption.displayName = 'TableCaption'
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableCaption,
|
TableCaption
|
||||||
}
|
};
|
||||||
@@ -1,52 +1,52 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
const TabsList = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
const TabsTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
const TabsContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
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 * as React from "react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Textarea = React.forwardRef<
|
const Textarea = React.forwardRef<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
React.ComponentProps<'textarea'>
|
React.ComponentProps<"textarea">
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
Textarea.displayName = 'Textarea'
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea };
|
||||||
@@ -1,126 +1,125 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as ToastPrimitives from '@radix-ui/react-toast'
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { X } from 'lucide-react'
|
import { X } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
const ToastViewport = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Viewport
|
<ToastPrimitives.Viewport
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||||
|
|
||||||
const toastVariants = cva(
|
const toastVariants = cva(
|
||||||
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'border bg-background text-foreground',
|
default: "border bg-background text-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
"destructive group border-destructive bg-destructive text-destructive-foreground"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: "default"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
const Toast = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
VariantProps<typeof toastVariants>
|
VariantProps<typeof toastVariants>
|
||||||
>(({ className, variant, ...props }, ref) => {
|
>(({ className, variant, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<ToastPrimitives.Root
|
<ToastPrimitives.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(toastVariants({ variant }), className)}
|
className={cn(toastVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
const ToastAction = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Action
|
<ToastPrimitives.Action
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
const ToastClose = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Close
|
<ToastPrimitives.Close
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
toast-close=''
|
toast-close=""
|
||||||
{...props}
|
{...props}>
|
||||||
>
|
<X className="h-4 w-4" />
|
||||||
<X className='h-4 w-4' />
|
</ToastPrimitives.Close>
|
||||||
</ToastPrimitives.Close>
|
));
|
||||||
))
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
const ToastTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Title
|
<ToastPrimitives.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
const ToastDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Description
|
<ToastPrimitives.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm opacity-90', className)}
|
className={cn("text-sm opacity-90", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||||
|
|
||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ToastProps,
|
type ToastProps,
|
||||||
type ToastActionElement,
|
type ToastActionElement,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
Toast,
|
Toast,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastAction,
|
ToastAction
|
||||||
}
|
};
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
import { useToast } from '@/hooks/use-toast'
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Toast,
|
Toast,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastViewport,
|
ToastViewport
|
||||||
} from '@/components/ui/toast'
|
} from "@/components/ui/toast";
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const { toasts } = useToast()
|
const { toasts } = useToast();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
return (
|
return (
|
||||||
<Toast key={id} {...props}>
|
<Toast key={id} {...props}>
|
||||||
<div className='grid gap-1'>
|
<div className="grid gap-1">
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
{description && (
|
{description && (
|
||||||
<ToastDescription>{description}</ToastDescription>
|
<ToastDescription>{description}</ToastDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{action}
|
{action}
|
||||||
<ToastClose />
|
<ToastClose />
|
||||||
</Toast>
|
</Toast>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
<ToastViewport />
|
<ToastViewport />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import { cn } from '@/lib/utils'
|
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<
|
const TooltipContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Content
|
<TooltipPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
))
|
));
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
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 React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
import { fonts } from '@/config/fonts'
|
import { fonts } from "@/config/fonts";
|
||||||
|
|
||||||
type Font = (typeof fonts)[number]
|
type Font = (typeof fonts)[number];
|
||||||
|
|
||||||
interface FontContextType {
|
interface FontContextType {
|
||||||
font: Font
|
font: Font;
|
||||||
setFont: (font: Font) => void
|
setFont: (font: Font) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FontContext = createContext<FontContextType | undefined>(undefined)
|
const FontContext = createContext<FontContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
|
export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const [font, _setFont] = useState<Font>(() => {
|
const [font, _setFont] = useState<Font>(() => {
|
||||||
const savedFont = localStorage.getItem('font')
|
const savedFont = localStorage.getItem("font");
|
||||||
return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
|
return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0];
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const applyFont = (font: string) => {
|
const applyFont = (font: string) => {
|
||||||
const root = document.documentElement
|
const root = document.documentElement;
|
||||||
root.classList.forEach((cls) => {
|
root.classList.forEach((cls) => {
|
||||||
if (cls.startsWith('font-')) root.classList.remove(cls)
|
if (cls.startsWith("font-")) root.classList.remove(cls);
|
||||||
})
|
});
|
||||||
root.classList.add(`font-${font}`)
|
root.classList.add(`font-${font}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
applyFont(font)
|
applyFont(font);
|
||||||
}, [font])
|
}, [font]);
|
||||||
|
|
||||||
const setFont = (font: Font) => {
|
const setFont = (font: Font) => {
|
||||||
localStorage.setItem('font', font)
|
localStorage.setItem("font", font);
|
||||||
_setFont(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
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const useFont = () => {
|
export const useFont = () => {
|
||||||
const context = useContext(FontContext)
|
const context = useContext(FontContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useFont must be used within a FontProvider')
|
throw new Error("useFont must be used within a FontProvider");
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
};
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { CommandMenu } from '@/components/command-menu'
|
import { CommandMenu } from "@/components/command-menu";
|
||||||
|
|
||||||
interface SearchContextType {
|
interface SearchContextType {
|
||||||
open: boolean
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchContext = React.createContext<SearchContextType | null>(null)
|
const SearchContext = React.createContext<SearchContextType | null>(null);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchProvider({ children }: Props) {
|
export function SearchProvider({ children }: Props) {
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
setOpen((open) => !open)
|
setOpen((open) => !open);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
document.addEventListener('keydown', down)
|
document.addEventListener("keydown", down);
|
||||||
return () => document.removeEventListener('keydown', down)
|
return () => document.removeEventListener("keydown", down);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchContext.Provider value={{ open, setOpen }}>
|
<SearchContext.Provider value={{ open, setOpen }}>
|
||||||
{children}
|
{children}
|
||||||
<CommandMenu />
|
<CommandMenu />
|
||||||
</SearchContext.Provider>
|
</SearchContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const useSearch = () => {
|
export const useSearch = () => {
|
||||||
const searchContext = React.useContext(SearchContext)
|
const searchContext = React.useContext(SearchContext);
|
||||||
|
|
||||||
if (!searchContext) {
|
if (!searchContext) {
|
||||||
throw new Error('useSearch has to be used within <SearchContext.Provider>')
|
throw new Error("useSearch has to be used within <SearchContext.Provider>");
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchContext
|
return searchContext;
|
||||||
}
|
};
|
||||||
@@ -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 = {
|
type ThemeProviderProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
defaultTheme?: Theme
|
defaultTheme?: Theme;
|
||||||
storageKey?: string
|
storageKey?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type ThemeProviderState = {
|
type ThemeProviderState = {
|
||||||
theme: Theme
|
theme: Theme;
|
||||||
setTheme: (theme: Theme) => void
|
setTheme: (theme: Theme) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const initialState: ThemeProviderState = {
|
const initialState: ThemeProviderState = {
|
||||||
theme: 'system',
|
theme: "system",
|
||||||
setTheme: () => null,
|
setTheme: () => null
|
||||||
}
|
};
|
||||||
|
|
||||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||||
|
|
||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
defaultTheme = 'system',
|
defaultTheme = "system",
|
||||||
storageKey = 'vite-ui-theme',
|
storageKey = "vite-ui-theme",
|
||||||
...props
|
...props
|
||||||
}: ThemeProviderProps) {
|
}: ThemeProviderProps) {
|
||||||
const [theme, _setTheme] = useState<Theme>(
|
const [theme, _setTheme] = useState<Theme>(
|
||||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||||
)
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = window.document.documentElement
|
const root = window.document.documentElement;
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
const applyTheme = (theme: Theme) => {
|
const applyTheme = (theme: Theme) => {
|
||||||
root.classList.remove('light', 'dark') // Remove existing theme classes
|
root.classList.remove("light", "dark"); // Remove existing theme classes
|
||||||
const systemTheme = mediaQuery.matches ? 'dark' : 'light'
|
const systemTheme = mediaQuery.matches ? "dark" : "light";
|
||||||
const effectiveTheme = theme === 'system' ? systemTheme : theme
|
const effectiveTheme = theme === "system" ? systemTheme : theme;
|
||||||
root.classList.add(effectiveTheme) // Add the new theme class
|
root.classList.add(effectiveTheme); // Add the new theme class
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleChange = () => {
|
const handleChange = () => {
|
||||||
if (theme === 'system') {
|
if (theme === "system") {
|
||||||
applyTheme('system')
|
applyTheme("system");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
applyTheme(theme)
|
applyTheme(theme);
|
||||||
|
|
||||||
mediaQuery.addEventListener('change', handleChange)
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
|
||||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||||
}, [theme])
|
}, [theme]);
|
||||||
|
|
||||||
const setTheme = (theme: Theme) => {
|
const setTheme = (theme: Theme) => {
|
||||||
localStorage.setItem(storageKey, theme)
|
localStorage.setItem(storageKey, theme);
|
||||||
_setTheme(theme)
|
_setTheme(theme);
|
||||||
}
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
theme,
|
theme,
|
||||||
setTheme,
|
setTheme
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProviderContext.Provider {...props} value={value}>
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
{children}
|
{children}
|
||||||
</ThemeProviderContext.Provider>
|
</ThemeProviderContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const useTheme = () => {
|
export const useTheme = () => {
|
||||||
const context = useContext(ThemeProviderContext)
|
const context = useContext(ThemeProviderContext);
|
||||||
|
|
||||||
if (context === undefined)
|
if (context === undefined)
|
||||||
throw new Error('useTheme must be used within a ThemeProvider')
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
};
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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