Arreglos layout
This commit is contained in:
parent
55a6b1fbaf
commit
1bbaf0aaee
@ -21,7 +21,7 @@ export function PageHeader({
|
||||
className,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-row items-center justify-between", className)}>
|
||||
<div className={cn("flex flex-row items-center justify-between mb-6", className)}>
|
||||
{/* Lado izquierdo */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start gap-4">
|
||||
@ -36,7 +36,7 @@ export function PageHeader({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold tracking-tight lg:text-2xl h-8 text-foreground sm:truncate sm:tracking-tight">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
@ -2,7 +2,6 @@ import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
@ -90,25 +89,23 @@ export function SelectField<TFormValues extends FieldValues>({
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? undefined}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-required={required}
|
||||
className={cn(
|
||||
"bg-muted/50 font-medium",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
inputClassName
|
||||
)}
|
||||
id={triggerId}
|
||||
>
|
||||
<SelectValue
|
||||
className={"placeholder:font-normal placeholder:italic"}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectTrigger
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-required={required}
|
||||
className={cn(
|
||||
"bg-muted/50 font-medium",
|
||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||
"placeholder:text-muted-foreground/50",
|
||||
inputClassName
|
||||
)}
|
||||
id={triggerId}
|
||||
>
|
||||
<SelectValue
|
||||
className={"placeholder:font-normal placeholder:italic"}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{normalizedItems.map((item) => (
|
||||
<SelectItem key={`key-${item.value}`} value={item.value}>
|
||||
|
||||
@ -7,6 +7,7 @@ export * from "./error-overlay.tsx";
|
||||
export * from "./form/index.ts";
|
||||
export * from "./full-screen-modal.tsx";
|
||||
export * from "./grid/index.ts";
|
||||
export * from "./initials-avatar.tsx";
|
||||
export * from "./layout/index.ts";
|
||||
export * from "./loading-overlay/index.ts";
|
||||
export * from "./logo-verifactu.tsx";
|
||||
|
||||
65
packages/rdx-ui/src/components/initials-avatar.tsx
Normal file
65
packages/rdx-ui/src/components/initials-avatar.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Avatar, AvatarFallback } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type * as React from "react";
|
||||
|
||||
type InitialsProps = {
|
||||
name?: string | null;
|
||||
maxParts?: number;
|
||||
fallback?: string;
|
||||
};
|
||||
|
||||
export const getInitials = ({ name, maxParts = 2, fallback = "?" }: InitialsProps): string => {
|
||||
if (!name?.trim()) return fallback;
|
||||
|
||||
return (
|
||||
name
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, maxParts)
|
||||
.map((p) => p[0]?.toUpperCase() ?? "")
|
||||
.join("") || fallback
|
||||
);
|
||||
};
|
||||
|
||||
export const Initials = (props: InitialsProps) => {
|
||||
return <>{getInitials(props)}</>;
|
||||
};
|
||||
|
||||
const VARIANT_CLASSES: Record<string, string> = {
|
||||
primary: "bg-primary/10 text-primary",
|
||||
muted: "bg-muted text-muted-foreground",
|
||||
};
|
||||
|
||||
type InitialsAvatarProps = React.ComponentProps<typeof Avatar> & {
|
||||
name?: string | null;
|
||||
variant?: "primary" | "muted";
|
||||
maxParts?: number;
|
||||
fallback?: string;
|
||||
fallbackClassName?: string;
|
||||
};
|
||||
|
||||
export const InitialsAvatar = ({
|
||||
name,
|
||||
variant = "muted",
|
||||
maxParts = 2,
|
||||
fallback = "?",
|
||||
className,
|
||||
fallbackClassName,
|
||||
...props
|
||||
}: InitialsAvatarProps) => {
|
||||
return (
|
||||
<Avatar
|
||||
className={cn(
|
||||
"size-10 border-2 shadow-sm",
|
||||
variant === "primary" && "border-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<AvatarFallback className={cn(VARIANT_CLASSES[variant], fallbackClassName)}>
|
||||
<Initials fallback={fallback} maxParts={maxParts} name={name} />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
@ -5,28 +5,20 @@ import {
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
Separator,
|
||||
SidebarTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
export const AppBreadcrumb = () => {
|
||||
return (
|
||||
<header className="app-breadcrumb flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2 px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator className="mr-2 h-4" orientation="vertical" />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,8 +7,8 @@ export const AppHeader = ({
|
||||
...props
|
||||
}: PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<div className={cn("app-header", className)} {...props}>
|
||||
<header className={cn("app-header space-y-3", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,26 +2,24 @@ import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
import { AppSidebar } from "./app-sidebar.tsx";
|
||||
import { SiteHeader } from "./site-header.tsx";
|
||||
import { AppTopbar } from "./app-topbar.tsx";
|
||||
|
||||
export const AppLayout = () => {
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||
"--header-height": "calc(var(--spacing) * 12)",
|
||||
"--sidebar-width": "16rem",
|
||||
"--sidebar-width-mobile": "18rem",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<AppSidebar className="bg-sidebar" variant="inset" />
|
||||
<AppSidebar className="bg-sidebar" />
|
||||
{/* Aquí está el MAIN */}
|
||||
<SidebarInset className="app-main bg-muted ">
|
||||
<SiteHeader />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="@container/main p-(--content-padding) xl:group-data-[theme-content-layout=centered]/layout:container xl:group-data-[theme-content-layout=centered]/layout:mx-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
<AppTopbar />
|
||||
<div className="flex flex-1 flex-col px-4 py-4 sm:px-6 sm:py-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarRail,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
CameraIcon,
|
||||
CircleIcon,
|
||||
@ -140,8 +146,8 @@ const data = {
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader className="mb-3">
|
||||
<Sidebar collapsible="icon" variant="sidebar" {...props}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={data.teams} />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
@ -151,6 +157,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
37
packages/rdx-ui/src/components/layout/app-topbar.tsx
Normal file
37
packages/rdx-ui/src/components/layout/app-topbar.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
Button,
|
||||
Separator,
|
||||
SidebarTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
import { AppBreadcrumb } from "./app-breadcrumb.tsx";
|
||||
import { ProfileDropdown } from "./dropdown-profile.tsx";
|
||||
|
||||
export function AppTopbar() {
|
||||
return (
|
||||
<header className="bg-card sticky top-0 z-50 border-b">
|
||||
<div className="flex items-center justify-between gap-6 px-4 py-2 sm:px-6">
|
||||
<div className="flex items-center gap-4 align-middle">
|
||||
<SidebarTrigger className="[&_svg]:size-5!" />
|
||||
<Separator className="hidden h-8! sm:block" orientation="vertical" />
|
||||
<AppBreadcrumb />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ProfileDropdown
|
||||
trigger={
|
||||
<Button className="size-9.5" size="icon" variant="ghost">
|
||||
<Avatar className="size-9.5 rounded-md">
|
||||
<AvatarImage src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png" />
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@ -1,263 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { useIsMobile } from "@repo/shadcn-ui/hooks/";
|
||||
|
||||
const chartData = [
|
||||
{ date: "2024-04-01", desktop: 222, mobile: 150 },
|
||||
{ date: "2024-04-02", desktop: 97, mobile: 180 },
|
||||
{ date: "2024-04-03", desktop: 167, mobile: 120 },
|
||||
{ date: "2024-04-04", desktop: 242, mobile: 260 },
|
||||
{ date: "2024-04-05", desktop: 373, mobile: 290 },
|
||||
{ date: "2024-04-06", desktop: 301, mobile: 340 },
|
||||
{ date: "2024-04-07", desktop: 245, mobile: 180 },
|
||||
{ date: "2024-04-08", desktop: 409, mobile: 320 },
|
||||
{ date: "2024-04-09", desktop: 59, mobile: 110 },
|
||||
{ date: "2024-04-10", desktop: 261, mobile: 190 },
|
||||
{ date: "2024-04-11", desktop: 327, mobile: 350 },
|
||||
{ date: "2024-04-12", desktop: 292, mobile: 210 },
|
||||
{ date: "2024-04-13", desktop: 342, mobile: 380 },
|
||||
{ date: "2024-04-14", desktop: 137, mobile: 220 },
|
||||
{ date: "2024-04-15", desktop: 120, mobile: 170 },
|
||||
{ date: "2024-04-16", desktop: 138, mobile: 190 },
|
||||
{ date: "2024-04-17", desktop: 446, mobile: 360 },
|
||||
{ date: "2024-04-18", desktop: 364, mobile: 410 },
|
||||
{ date: "2024-04-19", desktop: 243, mobile: 180 },
|
||||
{ date: "2024-04-20", desktop: 89, mobile: 150 },
|
||||
{ date: "2024-04-21", desktop: 137, mobile: 200 },
|
||||
{ date: "2024-04-22", desktop: 224, mobile: 170 },
|
||||
{ date: "2024-04-23", desktop: 138, mobile: 230 },
|
||||
{ date: "2024-04-24", desktop: 387, mobile: 290 },
|
||||
{ date: "2024-04-25", desktop: 215, mobile: 250 },
|
||||
{ date: "2024-04-26", desktop: 75, mobile: 130 },
|
||||
{ date: "2024-04-27", desktop: 383, mobile: 420 },
|
||||
{ date: "2024-04-28", desktop: 122, mobile: 180 },
|
||||
{ date: "2024-04-29", desktop: 315, mobile: 240 },
|
||||
{ date: "2024-04-30", desktop: 454, mobile: 380 },
|
||||
{ date: "2024-05-01", desktop: 165, mobile: 220 },
|
||||
{ date: "2024-05-02", desktop: 293, mobile: 310 },
|
||||
{ date: "2024-05-03", desktop: 247, mobile: 190 },
|
||||
{ date: "2024-05-04", desktop: 385, mobile: 420 },
|
||||
{ date: "2024-05-05", desktop: 481, mobile: 390 },
|
||||
{ date: "2024-05-06", desktop: 498, mobile: 520 },
|
||||
{ date: "2024-05-07", desktop: 388, mobile: 300 },
|
||||
{ date: "2024-05-08", desktop: 149, mobile: 210 },
|
||||
{ date: "2024-05-09", desktop: 227, mobile: 180 },
|
||||
{ date: "2024-05-10", desktop: 293, mobile: 330 },
|
||||
{ date: "2024-05-11", desktop: 335, mobile: 270 },
|
||||
{ date: "2024-05-12", desktop: 197, mobile: 240 },
|
||||
{ date: "2024-05-13", desktop: 197, mobile: 160 },
|
||||
{ date: "2024-05-14", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-05-15", desktop: 473, mobile: 380 },
|
||||
{ date: "2024-05-16", desktop: 338, mobile: 400 },
|
||||
{ date: "2024-05-17", desktop: 499, mobile: 420 },
|
||||
{ date: "2024-05-18", desktop: 315, mobile: 350 },
|
||||
{ date: "2024-05-19", desktop: 235, mobile: 180 },
|
||||
{ date: "2024-05-20", desktop: 177, mobile: 230 },
|
||||
{ date: "2024-05-21", desktop: 82, mobile: 140 },
|
||||
{ date: "2024-05-22", desktop: 81, mobile: 120 },
|
||||
{ date: "2024-05-23", desktop: 252, mobile: 290 },
|
||||
{ date: "2024-05-24", desktop: 294, mobile: 220 },
|
||||
{ date: "2024-05-25", desktop: 201, mobile: 250 },
|
||||
{ date: "2024-05-26", desktop: 213, mobile: 170 },
|
||||
{ date: "2024-05-27", desktop: 420, mobile: 460 },
|
||||
{ date: "2024-05-28", desktop: 233, mobile: 190 },
|
||||
{ date: "2024-05-29", desktop: 78, mobile: 130 },
|
||||
{ date: "2024-05-30", desktop: 340, mobile: 280 },
|
||||
{ date: "2024-05-31", desktop: 178, mobile: 230 },
|
||||
{ date: "2024-06-01", desktop: 178, mobile: 200 },
|
||||
{ date: "2024-06-02", desktop: 470, mobile: 410 },
|
||||
{ date: "2024-06-03", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-04", desktop: 439, mobile: 380 },
|
||||
{ date: "2024-06-05", desktop: 88, mobile: 140 },
|
||||
{ date: "2024-06-06", desktop: 294, mobile: 250 },
|
||||
{ date: "2024-06-07", desktop: 323, mobile: 370 },
|
||||
{ date: "2024-06-08", desktop: 385, mobile: 320 },
|
||||
{ date: "2024-06-09", desktop: 438, mobile: 480 },
|
||||
{ date: "2024-06-10", desktop: 155, mobile: 200 },
|
||||
{ date: "2024-06-11", desktop: 92, mobile: 150 },
|
||||
{ date: "2024-06-12", desktop: 492, mobile: 420 },
|
||||
{ date: "2024-06-13", desktop: 81, mobile: 130 },
|
||||
{ date: "2024-06-14", desktop: 426, mobile: 380 },
|
||||
{ date: "2024-06-15", desktop: 307, mobile: 350 },
|
||||
{ date: "2024-06-16", desktop: 371, mobile: 310 },
|
||||
{ date: "2024-06-17", desktop: 475, mobile: 520 },
|
||||
{ date: "2024-06-18", desktop: 107, mobile: 170 },
|
||||
{ date: "2024-06-19", desktop: 341, mobile: 290 },
|
||||
{ date: "2024-06-20", desktop: 408, mobile: 450 },
|
||||
{ date: "2024-06-21", desktop: 169, mobile: 210 },
|
||||
{ date: "2024-06-22", desktop: 317, mobile: 270 },
|
||||
{ date: "2024-06-23", desktop: 480, mobile: 530 },
|
||||
{ date: "2024-06-24", desktop: 132, mobile: 180 },
|
||||
{ date: "2024-06-25", desktop: 141, mobile: 190 },
|
||||
{ date: "2024-06-26", desktop: 434, mobile: 380 },
|
||||
{ date: "2024-06-27", desktop: 448, mobile: 490 },
|
||||
{ date: "2024-06-28", desktop: 149, mobile: 200 },
|
||||
{ date: "2024-06-29", desktop: 103, mobile: 160 },
|
||||
{ date: "2024-06-30", desktop: 446, mobile: 400 },
|
||||
];
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function ChartAreaInteractive() {
|
||||
const isMobile = useIsMobile();
|
||||
const [timeRange, setTimeRange] = React.useState("30d");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMobile) {
|
||||
setTimeRange("7d");
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date);
|
||||
const referenceDate = new Date("2024-06-30");
|
||||
let daysToSubtract = 90;
|
||||
if (timeRange === "30d") {
|
||||
daysToSubtract = 30;
|
||||
} else if (timeRange === "7d") {
|
||||
daysToSubtract = 7;
|
||||
}
|
||||
const startDate = new Date(referenceDate);
|
||||
startDate.setDate(startDate.getDate() - daysToSubtract);
|
||||
return date >= startDate;
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className='@container/card'>
|
||||
<CardHeader className='relative'>
|
||||
<CardTitle>Total Visitors</CardTitle>
|
||||
<CardDescription>
|
||||
<span className='@[540px]/card:block hidden'>Total for the last 3 months</span>
|
||||
<span className='@[540px]/card:hidden'>Last 3 months</span>
|
||||
</CardDescription>
|
||||
<div className='absolute right-4 top-4'>
|
||||
<ToggleGroup
|
||||
type='single'
|
||||
value={timeRange}
|
||||
onValueChange={setTimeRange}
|
||||
variant='outline'
|
||||
className='@[767px]/card:flex hidden'
|
||||
>
|
||||
<ToggleGroupItem value='90d' className='h-8 px-2.5'>
|
||||
Last 3 months
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value='30d' className='h-8 px-2.5'>
|
||||
Last 30 days
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value='7d' className='h-8 px-2.5'>
|
||||
Last 7 days
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger className='@[767px]/card:hidden flex w-40' aria-label='Select a value'>
|
||||
<SelectValue placeholder='Last 3 months' />
|
||||
</SelectTrigger>
|
||||
<SelectContent className='rounded-xl'>
|
||||
<SelectItem value='90d' className='rounded-lg'>
|
||||
Last 3 months
|
||||
</SelectItem>
|
||||
<SelectItem value='30d' className='rounded-lg'>
|
||||
Last 30 days
|
||||
</SelectItem>
|
||||
<SelectItem value='7d' className='rounded-lg'>
|
||||
Last 7 days
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className='px-2 pt-4 sm:px-6 sm:pt-6'>
|
||||
<ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>
|
||||
<AreaChart data={filteredData}>
|
||||
<defs>
|
||||
<linearGradient id='fillDesktop' x1='0' y1='0' x2='0' y2='1'>
|
||||
<stop offset='5%' stopColor='var(--color-desktop)' stopOpacity={1.0} />
|
||||
<stop offset='95%' stopColor='var(--color-desktop)' stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
<linearGradient id='fillMobile' x1='0' y1='0' x2='0' y2='1'>
|
||||
<stop offset='5%' stopColor='var(--color-mobile)' stopOpacity={0.8} />
|
||||
<stop offset='95%' stopColor='var(--color-mobile)' stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey='date'
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}}
|
||||
indicator='dot'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey='mobile'
|
||||
type='natural'
|
||||
fill='url(#fillMobile)'
|
||||
stroke='var(--color-mobile)'
|
||||
stackId='a'
|
||||
/>
|
||||
<Area
|
||||
dataKey='desktop'
|
||||
type='natural'
|
||||
fill='url(#fillDesktop)'
|
||||
stroke='var(--color-desktop)'
|
||||
stackId='a'
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -1,751 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
type UniqueIdentifier,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { Badge } from "@repo/shadcn-ui/components/badge";
|
||||
import { Button } from "@repo/shadcn-ui/components/button";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@repo/shadcn-ui/components/chart";
|
||||
import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components/dropdown-menu";
|
||||
import { Input } from "@repo/shadcn-ui/components/input";
|
||||
import { Label } from "@repo/shadcn-ui/components/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/shadcn-ui/components/select";
|
||||
import { Separator } from "@repo/shadcn-ui/components/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@repo/shadcn-ui/components/sheet";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@repo/shadcn-ui/components/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/shadcn-ui/components/tabs";
|
||||
import { useIsMobile } from "@repo/shadcn-ui/hooks";
|
||||
import {
|
||||
type ColumnDef,
|
||||
type ColumnFiltersState,
|
||||
type Row,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
CheckCircle2Icon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
ColumnsIcon,
|
||||
GripVerticalIcon,
|
||||
LoaderIcon,
|
||||
MoreVerticalIcon,
|
||||
PlusIcon,
|
||||
TrendingUpIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const schema = z.object({
|
||||
id: z.number(),
|
||||
header: z.string(),
|
||||
type: z.string(),
|
||||
status: z.string(),
|
||||
target: z.string(),
|
||||
limit: z.string(),
|
||||
reviewer: z.string(),
|
||||
});
|
||||
|
||||
// Create a separate component for the drag handle
|
||||
function DragHandle({ id }: { id: number }) {
|
||||
const { attributes, listeners } = useSortable({
|
||||
id,
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="size-7 text-muted-foreground hover:bg-transparent"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<GripVerticalIcon className="size-3 text-muted-foreground" />
|
||||
<span className="sr-only">Drag to reorder</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
{
|
||||
id: "drag",
|
||||
header: () => null,
|
||||
cell: ({ row }) => <DragHandle id={row.original.id} />,
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
aria-label="Select all"
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
aria-label="Select row"
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "header",
|
||||
header: "Header",
|
||||
cell: ({ row }) => {
|
||||
return <TableCellViewer item={row.original} />;
|
||||
},
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: "Section Type",
|
||||
cell: ({ row }) => (
|
||||
<div className="w-32">
|
||||
<Badge className="px-1.5 text-muted-foreground" variant="outline">
|
||||
{row.original.type}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge className="flex gap-1 px-1.5 text-muted-foreground [&_svg]:size-3" variant="outline">
|
||||
{row.original.status === "Done" ? (
|
||||
<CheckCircle2Icon className="text-green-500 dark:text-green-400" />
|
||||
) : (
|
||||
<LoaderIcon />
|
||||
)}
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "target",
|
||||
header: () => <div className="w-full text-right">Target</div>,
|
||||
cell: ({ row }) => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Label className="sr-only" htmlFor={`${row.original.id}-target`}>
|
||||
Target
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background"
|
||||
defaultValue={row.original.target}
|
||||
id={`${row.original.id}-target`}
|
||||
/>
|
||||
</form>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "limit",
|
||||
header: () => <div className="w-full text-right">Limit</div>,
|
||||
cell: ({ row }) => (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||
loading: `Saving ${row.original.header}`,
|
||||
success: "Done",
|
||||
error: "Error",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Label className="sr-only" htmlFor={`${row.original.id}-limit`}>
|
||||
Limit
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background"
|
||||
defaultValue={row.original.limit}
|
||||
id={`${row.original.id}-limit`}
|
||||
/>
|
||||
</form>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "reviewer",
|
||||
header: "Reviewer",
|
||||
cell: ({ row }) => {
|
||||
const isAssigned = row.original.reviewer !== "Assign reviewer";
|
||||
|
||||
if (isAssigned) {
|
||||
return row.original.reviewer;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label className="sr-only" htmlFor={`${row.original.id}-reviewer`}>
|
||||
Reviewer
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger className="h-8 w-40" id={`${row.original.id}-reviewer`}>
|
||||
<SelectValue placeholder="Assign reviewer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||
<SelectItem value="Jamik Tashpulatov">Jamik Tashpulatov</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: () => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
||||
<DropdownMenuItem>Favorite</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
|
||||
const { transform, transition, setNodeRef, isDragging } = useSortable({
|
||||
id: row.original.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||
data-dragging={isDragging}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: transition,
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function DataTable({ data: initialData }: { data: z.infer<typeof schema>[] }) {
|
||||
const [data, setData] = React.useState(() => initialData);
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [pagination, setPagination] = React.useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const sortableId = React.useId();
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
useSensor(TouchSensor, {}),
|
||||
useSensor(KeyboardSensor, {})
|
||||
);
|
||||
|
||||
const dataIds = React.useMemo<UniqueIdentifier[]>(() => data?.map(({ id }) => id) || [], [data]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
pagination,
|
||||
},
|
||||
getRowId: (row) => row.id.toString(),
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
});
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
if (active && over && active.id !== over.id) {
|
||||
setData((data) => {
|
||||
const oldIndex = dataIds.indexOf(active.id);
|
||||
const newIndex = dataIds.indexOf(over.id);
|
||||
return arrayMove(data, oldIndex, newIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs className="flex w-full flex-col justify-start gap-6" defaultValue="outline">
|
||||
<div className="flex items-center justify-between px-4 lg:px-6">
|
||||
<Label className="sr-only" htmlFor="view-selector">
|
||||
View
|
||||
</Label>
|
||||
<Select defaultValue="outline">
|
||||
<SelectTrigger className="@4xl/main:hidden flex w-fit" id="view-selector">
|
||||
<SelectValue placeholder="Select a view" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="outline">Outline</SelectItem>
|
||||
<SelectItem value="past-performance">Past Performance</SelectItem>
|
||||
<SelectItem value="key-personnel">Key Personnel</SelectItem>
|
||||
<SelectItem value="focus-documents">Focus Documents</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TabsList className="@4xl/main:flex hidden">
|
||||
<TabsTrigger value="outline">Outline</TabsTrigger>
|
||||
<TabsTrigger className="gap-1" value="past-performance">
|
||||
Past Performance{" "}
|
||||
<Badge
|
||||
className="flex size-5 items-center justify-center rounded-full bg-muted-foreground/30"
|
||||
variant="secondary"
|
||||
>
|
||||
3
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="gap-1" value="key-personnel">
|
||||
Key Personnel{" "}
|
||||
<Badge
|
||||
className="flex size-5 items-center justify-center rounded-full bg-muted-foreground/30"
|
||||
variant="secondary"
|
||||
>
|
||||
2
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button size="sm" variant="outline">
|
||||
<ColumnsIcon />
|
||||
<span className="hidden lg:inline">Customize Columns</span>
|
||||
<span className="lg:hidden">Columns</span>
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={column.getIsVisible()}
|
||||
className="capitalize"
|
||||
key={column.id}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button size="sm" variant="outline">
|
||||
<PlusIcon />
|
||||
<span className="hidden lg:inline">Add Section</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent
|
||||
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
|
||||
value="outline"
|
||||
>
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
id={sortableId}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10 bg-muted">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead colSpan={header.colSpan} key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="**:data-[slot=table-cell]:first:w-8">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
<SortableContext items={dataIds} strategy={verticalListSortingStrategy}>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<DraggableRow key={row.id} row={row} />
|
||||
))}
|
||||
</SortableContext>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="h-24 text-center" colSpan={columns.length}>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-8 lg:w-fit">
|
||||
<div className="hidden items-center gap-2 lg:flex">
|
||||
<Label className="text-sm font-medium" htmlFor="rows-per-page">
|
||||
Rows per page
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
>
|
||||
<SelectTrigger className="w-20" id="rows-per-page">
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-fit items-center justify-center text-sm font-medium">
|
||||
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 lg:ml-0">
|
||||
<Button
|
||||
className="hidden size-8 p-0 lg:flex"
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
variant="outline"
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<ChevronsLeftIcon />
|
||||
</Button>
|
||||
<Button
|
||||
className="size-8"
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
onClick={() => table.previousPage()}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<ChevronLeftIcon />
|
||||
</Button>
|
||||
<Button
|
||||
className="size-8"
|
||||
disabled={!table.getCanNextPage()}
|
||||
onClick={() => table.nextPage()}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
<Button
|
||||
className="hidden size-8 lg:flex"
|
||||
disabled={!table.getCanNextPage()}
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<ChevronsRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent className="flex flex-col px-4 lg:px-6" value="past-performance">
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
|
||||
</TabsContent>
|
||||
<TabsContent className="flex flex-col px-4 lg:px-6" value="key-personnel">
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
|
||||
</TabsContent>
|
||||
<TabsContent className="flex flex-col px-4 lg:px-6" value="focus-documents">
|
||||
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
const chartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 214, mobile: 140 },
|
||||
];
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--primary)",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<Button className="w-fit px-0 text-left text-foreground" variant="link">
|
||||
{item.header}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="flex flex-col" side="right">
|
||||
<SheetHeader className="gap-1">
|
||||
<SheetTitle>{item.header}</SheetTitle>
|
||||
<SheetDescription>Showing total visitors for the last 6 months</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-1 flex-col gap-4 overflow-y-auto py-4 text-sm">
|
||||
{!isMobile && (
|
||||
<>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 10,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
dataKey="month"
|
||||
hide
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent indicator="dot" />} cursor={false} />
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.6}
|
||||
stackId="a"
|
||||
stroke="var(--color-mobile)"
|
||||
type="natural"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stackId="a"
|
||||
stroke="var(--color-desktop)"
|
||||
type="natural"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
<Separator />
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUpIcon className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Showing total visitors for the last 6 months. This is just some random text to
|
||||
test the layout. It spans multiple lines and should wrap around.
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<form className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="header">Header</Label>
|
||||
<Input defaultValue={item.header} id="header" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="type">Type</Label>
|
||||
<Select defaultValue={item.type}>
|
||||
<SelectTrigger className="w-full" id="type">
|
||||
<SelectValue placeholder="Select a type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Table of Contents">Table of Contents</SelectItem>
|
||||
<SelectItem value="Executive Summary">Executive Summary</SelectItem>
|
||||
<SelectItem value="Technical Approach">Technical Approach</SelectItem>
|
||||
<SelectItem value="Design">Design</SelectItem>
|
||||
<SelectItem value="Capabilities">Capabilities</SelectItem>
|
||||
<SelectItem value="Focus Documents">Focus Documents</SelectItem>
|
||||
<SelectItem value="Narrative">Narrative</SelectItem>
|
||||
<SelectItem value="Cover Page">Cover Page</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select defaultValue={item.status}>
|
||||
<SelectTrigger className="w-full" id="status">
|
||||
<SelectValue placeholder="Select a status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Done">Done</SelectItem>
|
||||
<SelectItem value="In Progress">In Progress</SelectItem>
|
||||
<SelectItem value="Not Started">Not Started</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="target">Target</Label>
|
||||
<Input defaultValue={item.target} id="target" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="limit">Limit</Label>
|
||||
<Input defaultValue={item.limit} id="limit" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="reviewer">Reviewer</Label>
|
||||
<Select defaultValue={item.reviewer}>
|
||||
<SelectTrigger className="w-full" id="reviewer">
|
||||
<SelectValue placeholder="Select a reviewer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
|
||||
<SelectItem value="Jamik Tashpulatov">Jamik Tashpulatov</SelectItem>
|
||||
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<SheetFooter className="mt-auto flex gap-2 sm:flex-col sm:space-x-0">
|
||||
<Button className="w-full">Submit</Button>
|
||||
<SheetClose>
|
||||
<Button className="w-full" variant="outline">
|
||||
Done
|
||||
</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
95
packages/rdx-ui/src/components/layout/dropdown-profile.tsx
Normal file
95
packages/rdx-ui/src/components/layout/dropdown-profile.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
CirclePlusIcon,
|
||||
CreditCardIcon,
|
||||
LogOutIcon,
|
||||
SettingsIcon,
|
||||
SquarePenIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
trigger: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
align?: "start" | "center" | "end";
|
||||
};
|
||||
|
||||
export const ProfileDropdown = ({ trigger, defaultOpen, align = "end" }: Props) => {
|
||||
return (
|
||||
<DropdownMenu defaultOpen={defaultOpen}>
|
||||
<DropdownMenuTrigger>{trigger}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={align || "end"} className="w-80">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="flex items-center gap-4 px-4 py-2.5 font-normal">
|
||||
<div className="relative">
|
||||
<Avatar className="size-10">
|
||||
<AvatarImage
|
||||
alt="John Doe"
|
||||
src="https://cdn.shadcnstudio.com/ss-assets/avatar/avatar-1.png"
|
||||
/>
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="ring-card absolute right-0 bottom-0 block size-2 rounded-full bg-green-600 ring-2" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col items-start">
|
||||
<span className="text-foreground text-lg font-semibold">John Doe</span>
|
||||
<span className="text-muted-foreground text-base">john.doe@example.com</span>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
||||
<UserIcon className="text-foreground size-5" />
|
||||
<span>My account</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
||||
<SettingsIcon className="text-foreground size-5" />
|
||||
<span>Settings</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
||||
<CreditCardIcon className="text-foreground size-5" />
|
||||
<span>Billing</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
||||
<UsersIcon className="text-foreground size-5" />
|
||||
<span>Manage team</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
||||
<SquarePenIcon className="text-foreground size-5" />
|
||||
<span>Customization</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="px-4 py-2.5 text-base">
|
||||
<CirclePlusIcon className="text-foreground size-5" />
|
||||
<span>Add team account</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem className="px-4 py-2.5 text-base" variant="destructive">
|
||||
<LogOutIcon className="size-5" />
|
||||
<span>Logout</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./app-breadcrumb.tsx";
|
||||
export * from "./app-content.tsx";
|
||||
export * from "./app-header.tsx";
|
||||
export * from "./app-layout.tsx";
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components/dropdown-menu";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@repo/shadcn-ui/components/sidebar";
|
||||
import { FolderIcon, type LucideIcon, MoreHorizontalIcon, ShareIcon } from "lucide-react";
|
||||
|
||||
export function NavDocuments({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: LucideIcon;
|
||||
}[];
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Documents</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<SidebarMenuAction className="rounded-sm data-[state=open]:bg-accent" showOnHover>
|
||||
<MoreHorizontalIcon />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align={isMobile ? "end" : "start"}
|
||||
className="w-24 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<FolderIcon />
|
||||
<span>Open</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ShareIcon />
|
||||
<span>Share</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<MoreHorizontalIcon className="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
@ -3,7 +3,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@ -11,66 +11,53 @@ import {
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { ChevronRightIcon, type LucideIcon, PlusCircleIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { ChevronRightIcon, type LucideIcon } from "lucide-react";
|
||||
|
||||
type NavMainItem = {
|
||||
title: string;
|
||||
url?: string;
|
||||
icon?: LucideIcon;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
url?: string;
|
||||
icon?: LucideIcon;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export function NavMain({ items }: { items: NavMainItem[] }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex items-center gap-2">
|
||||
<SidebarMenuButton
|
||||
className="hidden min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
|
||||
tooltip="Quick Create"
|
||||
>
|
||||
<PlusCircleIcon />
|
||||
<span>Quick Create</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible asChild className="group/collapsible" defaultOpen={true} key={item.title}>
|
||||
<SidebarMenuItem className="mb-6">
|
||||
<CollapsibleTrigger>
|
||||
<SidebarMenuButton tooltip={item.title}>
|
||||
{item.icon && <item.icon />}
|
||||
<span className="font-semibold">{item.title}</span>
|
||||
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton>
|
||||
<a href={subItem.url}>
|
||||
<SidebarGroupLabel>Módulos</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible className="group/collapsible" defaultOpen={item.isActive} key={item.title}>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger render={<SidebarMenuButton tooltip={item.title} />}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton
|
||||
render={
|
||||
<a href={subItem.url} title={subItem.title}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
}
|
||||
/>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,82 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components/dropdown-menu";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@repo/shadcn-ui/components/sidebar";
|
||||
import { Folder, Forward, type LucideIcon, MoreHorizontal, Trash2 } from "lucide-react";
|
||||
|
||||
export function NavProjects({
|
||||
projects,
|
||||
}: {
|
||||
projects: {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: LucideIcon;
|
||||
}[];
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{projects.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<SidebarMenuAction showOnHover>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align={isMobile ? "end" : "start"}
|
||||
className="w-48 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<Folder className="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Forward className="text-muted-foreground" />
|
||||
<span>Share Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||
<MoreHorizontal className="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
@ -16,13 +16,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@repo/shadcn-ui/components/sidebar";
|
||||
import {
|
||||
BellIcon,
|
||||
CreditCardIcon,
|
||||
LogOutIcon,
|
||||
MoreVerticalIcon,
|
||||
UserCircleIcon,
|
||||
} from "lucide-react";
|
||||
import { BadgeCheck, Bell, ChevronsUpDownIcon, CreditCard, LogOut, Sparkles } from "lucide-react";
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
@ -34,63 +28,72 @@ export function NavUser({
|
||||
};
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<SidebarMenuButton
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
size="lg"
|
||||
>
|
||||
<Avatar className="size-8 rounded-lg grayscale">
|
||||
<AvatarImage alt={user.name} src={user.avatar} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
||||
</div>
|
||||
<MoreVerticalIcon className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="size-8 rounded-lg">
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<SidebarMenuButton
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
size="lg"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage alt={user.name} src={user.avatar} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<ChevronsUpDownIcon className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage alt={user.name} src={user.avatar} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<UserCircleIcon />
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCardIcon />
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<BellIcon />
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOutIcon />
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarInput,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { BinocularsIcon, SearchIcon } from "lucide-react";
|
||||
|
||||
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form {...props}>
|
||||
<SidebarGroup className="py-0">
|
||||
<SidebarGroupContent className="relative">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="search">
|
||||
Searchsss
|
||||
</Label>
|
||||
<SidebarInput className="pl-8 h-9" id="search" placeholder="Search..." />
|
||||
<SearchIcon className="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none " />
|
||||
<Button
|
||||
className="h-9 w-9 shrink-0 group-data-[collapsible=icon]:opacity-0"
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<BinocularsIcon />
|
||||
<span className="sr-only">Advanced search</span>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Badge } from "@repo/shadcn-ui/components/badge";
|
||||
|
||||
export function SectionCards() {
|
||||
return (
|
||||
<div className='*:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @5xl/main:grid-cols-4 grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card lg:px-6'>
|
||||
<Card className='@container/card'>
|
||||
<CardHeader className='relative'>
|
||||
<CardDescription>Total Revenue</CardDescription>
|
||||
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
|
||||
$1,250.00
|
||||
</CardTitle>
|
||||
<div className='absolute right-4 top-4'>
|
||||
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
|
||||
<TrendingUpIcon className='size-3' />
|
||||
+12.5%
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className='flex-col items-start gap-1 text-sm'>
|
||||
<div className='line-clamp-1 flex gap-2 font-medium'>
|
||||
Trending up this month <TrendingUpIcon className='size-4' />
|
||||
</div>
|
||||
<div className='text-muted-foreground'>Visitors for the last 6 months</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className='@container/card'>
|
||||
<CardHeader className='relative'>
|
||||
<CardDescription>New Customers</CardDescription>
|
||||
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
|
||||
1,234
|
||||
</CardTitle>
|
||||
<div className='absolute right-4 top-4'>
|
||||
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
|
||||
<TrendingDownIcon className='size-3' />
|
||||
-20%
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className='flex-col items-start gap-1 text-sm'>
|
||||
<div className='line-clamp-1 flex gap-2 font-medium'>
|
||||
Down 20% this period <TrendingDownIcon className='size-4' />
|
||||
</div>
|
||||
<div className='text-muted-foreground'>Acquisition needs attention</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className='@container/card'>
|
||||
<CardHeader className='relative'>
|
||||
<CardDescription>Active Accounts</CardDescription>
|
||||
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
|
||||
45,678
|
||||
</CardTitle>
|
||||
<div className='absolute right-4 top-4'>
|
||||
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
|
||||
<TrendingUpIcon className='size-3' />
|
||||
+12.5%
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className='flex-col items-start gap-1 text-sm'>
|
||||
<div className='line-clamp-1 flex gap-2 font-medium'>
|
||||
Strong user retention <TrendingUpIcon className='size-4' />
|
||||
</div>
|
||||
<div className='text-muted-foreground'>Engagement exceed targets</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Card className='@container/card'>
|
||||
<CardHeader className='relative'>
|
||||
<CardDescription>Growth Rate</CardDescription>
|
||||
<CardTitle className='@[250px]/card:text-3xl text-2xl font-semibold tabular-nums'>
|
||||
4.5%
|
||||
</CardTitle>
|
||||
<div className='absolute right-4 top-4'>
|
||||
<Badge variant='outline' className='flex gap-1 rounded-lg text-xs'>
|
||||
<TrendingUpIcon className='size-3' />
|
||||
+4.5%
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className='flex-col items-start gap-1 text-sm'>
|
||||
<div className='line-clamp-1 flex gap-2 font-medium'>
|
||||
Steady performance <TrendingUpIcon className='size-4' />
|
||||
</div>
|
||||
<div className='text-muted-foreground'>Meets growth projections</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,237 +0,0 @@
|
||||
import { Button, Separator, SidebarTrigger } from "@repo/shadcn-ui/components";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="bg-background sticky top-0 z-50 flex h-(--header-height) shrink-0 items-center gap-2 border-b backdrop-blur-md transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height) md:rounded-tl-xl md:rounded-tr-xl">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator className="mx-2 data-[orientation=vertical]:h-4" orientation="vertical" />
|
||||
<div className="lg:flex-1">
|
||||
<div className="relative hidden max-w-sm flex-1 lg:block">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="lucide lucide-search text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
|
||||
<input
|
||||
className="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-w-0 bg-transparent px-3 py-1 transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-9 w-full cursor-pointer rounded-md border pr-4 pl-10 text-sm shadow-xs"
|
||||
data-slot="input"
|
||||
placeholder="Search..."
|
||||
type="search"
|
||||
/>
|
||||
<div className="absolute top-1/2 right-2 hidden -translate-y-1/2 items-center gap-0.5 rounded-sm bg-zinc-200 p-1 font-mono text-xs font-medium sm:flex dark:bg-neutral-700">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="lucide lucide-command size-3"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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>
|
||||
<span>k</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="block lg:hidden">
|
||||
<button
|
||||
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-9"
|
||||
data-size="icon"
|
||||
data-slot="button"
|
||||
data-variant="ghost"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="lucide lucide-search"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col gap-2 text-center sm:text-left sr-only"
|
||||
data-slot="dialog-header"
|
||||
>
|
||||
<h2
|
||||
className="text-lg leading-none font-semibold"
|
||||
data-slot="dialog-title"
|
||||
id="radix-_R_rd5ubplbH1_"
|
||||
>
|
||||
Command Palette
|
||||
</h2>
|
||||
<p
|
||||
className="text-muted-foreground text-sm"
|
||||
data-slot="dialog-description"
|
||||
id="radix-_R_rd5ubplbH2_"
|
||||
>
|
||||
Search for a command to run...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button asChild className="hidden sm:flex" size="sm">
|
||||
<Link
|
||||
className="dark:text-foreground"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
to="https://shadcnuikit.com/"
|
||||
>
|
||||
Get Pro
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<a
|
||||
className="inline-flex items-center justify-center whitespace-nowrap text-sm transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive underline-offset-4 hover:underline h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 relative animate-pulse bg-linear-to-r from-violet-600 via-fuchsia-600 to-cyan-600 bg-clip-text font-medium text-transparent hover:bg-transparent"
|
||||
data-size="sm"
|
||||
data-slot="button"
|
||||
data-variant="link"
|
||||
href="https://shadcnuikit.com/pricing"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Get Pro
|
||||
</a>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8 relative"
|
||||
data-size="icon-sm"
|
||||
data-slot="dropdown-menu-trigger"
|
||||
data-state="closed"
|
||||
data-variant="ghost"
|
||||
id="radix-_R_kd5ubplb_"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="lucide lucide-bell"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
||||
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" />
|
||||
</svg>
|
||||
<span className="bg-destructive absolute end-0.5 top-0.5 block size-1.5 shrink-0 rounded-full" />
|
||||
</button>
|
||||
<button
|
||||
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8 relative"
|
||||
data-size="icon-sm"
|
||||
data-slot="button"
|
||||
data-variant="ghost"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="lucide lucide-moon"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</button>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 size-8"
|
||||
data-size="icon-sm"
|
||||
data-slot="dropdown-menu-trigger"
|
||||
data-state="closed"
|
||||
data-variant="ghost"
|
||||
id="radix-_R_14d5ubplb_"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="lucide lucide-palette"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
|
||||
<circle cx="13.5" cy="6.5" fill="currentColor" r=".5" />
|
||||
<circle cx="17.5" cy="10.5" fill="currentColor" r=".5" />
|
||||
<circle cx="6.5" cy="12.5" fill="currentColor" r=".5" />
|
||||
<circle cx="8.5" cy="7.5" fill="currentColor" r=".5" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
className="bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px mx-2 data-[orientation=vertical]:h-4"
|
||||
data-orientation="vertical"
|
||||
data-slot="separator"
|
||||
role="none"
|
||||
/>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
className="group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6"
|
||||
data-size="default"
|
||||
data-slot="dropdown-menu-trigger"
|
||||
data-state="closed"
|
||||
id="radix-_R_1kd5ubplb_"
|
||||
type="button"
|
||||
>
|
||||
<img
|
||||
alt="shadcn ui kit"
|
||||
className="aspect-square size-full object-cover"
|
||||
data-slot="avatar-image"
|
||||
src="/images/avatars/01.png"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
@ -36,48 +37,53 @@ export function TeamSwitcher({
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<SidebarMenuButton
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
size="lg"
|
||||
>
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<activeTeam.logo className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{activeTeam.name}</span>
|
||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<SidebarMenuButton
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
size="lg"
|
||||
>
|
||||
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<activeTeam.logo className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{activeTeam.name}</span>
|
||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
|
||||
{teams.map((team, index) => (
|
||||
<DropdownMenuItem
|
||||
className="gap-2 p-2"
|
||||
key={team.name}
|
||||
onClick={() => setActiveTeam(team)}
|
||||
>
|
||||
<div className="flex size-6 items-center justify-center rounded-md border">
|
||||
<team.logo className="size-3.5 shrink-0" />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
|
||||
{teams.map((team, index) => (
|
||||
<DropdownMenuItem
|
||||
className="gap-2 p-2"
|
||||
key={team.name}
|
||||
onClick={() => setActiveTeam(team)}
|
||||
>
|
||||
<div className="flex size-6 items-center justify-center rounded-md border">
|
||||
<team.logo className="size-3.5 shrink-0" />
|
||||
</div>
|
||||
{team.name}
|
||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2 p-2">
|
||||
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
|
||||
<Plus className="size-4" />
|
||||
</div>
|
||||
{team.name}
|
||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
||||
<div className="text-muted-foreground font-medium">Add team</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2 p-2">
|
||||
<div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
|
||||
<Plus className="size-4" />
|
||||
</div>
|
||||
<div className="text-muted-foreground font-medium">Add team</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user