Arreglos layout
This commit is contained in:
parent
55a6b1fbaf
commit
1bbaf0aaee
@ -21,7 +21,7 @@ export function PageHeader({
|
|||||||
className,
|
className,
|
||||||
}: PageHeaderProps) {
|
}: PageHeaderProps) {
|
||||||
return (
|
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 */}
|
{/* Lado izquierdo */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
@ -36,7 +36,7 @@ export function PageHeader({
|
|||||||
</Button>
|
</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">
|
<h1 className="text-xl font-semibold tracking-tight lg:text-2xl h-8 text-foreground sm:truncate sm:tracking-tight">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import {
|
|||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldError,
|
FieldError,
|
||||||
FormControl,
|
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
@ -90,25 +89,23 @@ export function SelectField<TFormValues extends FieldValues>({
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value ?? undefined}
|
value={field.value ?? undefined}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<SelectTrigger
|
||||||
<SelectTrigger
|
aria-invalid={fieldState.invalid}
|
||||||
aria-invalid={fieldState.invalid}
|
aria-required={required}
|
||||||
aria-required={required}
|
className={cn(
|
||||||
className={cn(
|
"bg-muted/50 font-medium",
|
||||||
"bg-muted/50 font-medium",
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
"placeholder:text-muted-foreground/50",
|
||||||
"placeholder:text-muted-foreground/50",
|
inputClassName
|
||||||
inputClassName
|
)}
|
||||||
)}
|
id={triggerId}
|
||||||
id={triggerId}
|
>
|
||||||
>
|
<SelectValue
|
||||||
<SelectValue
|
className={"placeholder:font-normal placeholder:italic"}
|
||||||
className={"placeholder:font-normal placeholder:italic"}
|
placeholder={placeholder}
|
||||||
placeholder={placeholder}
|
/>
|
||||||
/>
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{normalizedItems.map((item) => (
|
{normalizedItems.map((item) => (
|
||||||
<SelectItem key={`key-${item.value}`} value={item.value}>
|
<SelectItem key={`key-${item.value}`} value={item.value}>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export * from "./error-overlay.tsx";
|
|||||||
export * from "./form/index.ts";
|
export * from "./form/index.ts";
|
||||||
export * from "./full-screen-modal.tsx";
|
export * from "./full-screen-modal.tsx";
|
||||||
export * from "./grid/index.ts";
|
export * from "./grid/index.ts";
|
||||||
|
export * from "./initials-avatar.tsx";
|
||||||
export * from "./layout/index.ts";
|
export * from "./layout/index.ts";
|
||||||
export * from "./loading-overlay/index.ts";
|
export * from "./loading-overlay/index.ts";
|
||||||
export * from "./logo-verifactu.tsx";
|
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,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
Separator,
|
|
||||||
SidebarTrigger,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
export const AppBreadcrumb = () => {
|
export const AppBreadcrumb = () => {
|
||||||
return (
|
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">
|
<Breadcrumb>
|
||||||
<div className="flex items-center gap-2 px-6">
|
<BreadcrumbList>
|
||||||
<SidebarTrigger className="-ml-1" />
|
<BreadcrumbItem className="hidden md:block">
|
||||||
<Separator className="mr-2 h-4" orientation="vertical" />
|
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
|
||||||
<Breadcrumb>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbList>
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
<BreadcrumbItem className="hidden md:block">
|
<BreadcrumbItem>
|
||||||
<BreadcrumbLink href="#">Building Your Application</BreadcrumbLink>
|
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
</BreadcrumbList>
|
||||||
<BreadcrumbItem>
|
</Breadcrumb>
|
||||||
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,8 +7,8 @@ export const AppHeader = ({
|
|||||||
...props
|
...props
|
||||||
}: PropsWithChildren<{ className?: string }>) => {
|
}: PropsWithChildren<{ className?: string }>) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn("app-header", className)} {...props}>
|
<header className={cn("app-header space-y-3", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,26 +2,24 @@ import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components";
|
|||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
import { AppSidebar } from "./app-sidebar.tsx";
|
import { AppSidebar } from "./app-sidebar.tsx";
|
||||||
import { SiteHeader } from "./site-header.tsx";
|
import { AppTopbar } from "./app-topbar.tsx";
|
||||||
|
|
||||||
export const AppLayout = () => {
|
export const AppLayout = () => {
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
"--sidebar-width": "16rem",
|
||||||
"--header-height": "calc(var(--spacing) * 12)",
|
"--sidebar-width-mobile": "18rem",
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AppSidebar className="bg-sidebar" variant="inset" />
|
<AppSidebar className="bg-sidebar" />
|
||||||
{/* Aquí está el MAIN */}
|
{/* Aquí está el MAIN */}
|
||||||
<SidebarInset className="app-main bg-muted ">
|
<SidebarInset className="app-main bg-muted ">
|
||||||
<SiteHeader />
|
<AppTopbar />
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col px-4 py-4 sm:px-6 sm:py-6">
|
||||||
<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 />
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</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 {
|
import {
|
||||||
CameraIcon,
|
CameraIcon,
|
||||||
CircleIcon,
|
CircleIcon,
|
||||||
@ -140,8 +146,8 @@ const data = {
|
|||||||
|
|
||||||
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" variant="sidebar" {...props}>
|
||||||
<SidebarHeader className="mb-3">
|
<SidebarHeader>
|
||||||
<TeamSwitcher teams={data.teams} />
|
<TeamSwitcher teams={data.teams} />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
@ -151,6 +157,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser user={data.user} />
|
<NavUser user={data.user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
|
<SidebarRail />
|
||||||
</Sidebar>
|
</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-content.tsx";
|
||||||
export * from "./app-header.tsx";
|
export * from "./app-header.tsx";
|
||||||
export * from "./app-layout.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,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupLabel,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
@ -11,66 +11,53 @@ import {
|
|||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { ChevronRightIcon, type LucideIcon, PlusCircleIcon } from "lucide-react";
|
import { ChevronRightIcon, type LucideIcon } from "lucide-react";
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
|
|
||||||
type NavMainItem = {
|
export function NavMain({
|
||||||
title: string;
|
items,
|
||||||
url?: string;
|
}: {
|
||||||
icon?: LucideIcon;
|
items: {
|
||||||
isActive?: boolean;
|
|
||||||
items?: {
|
|
||||||
title: string;
|
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 (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupContent className="flex flex-col gap-2">
|
<SidebarGroupLabel>Módulos</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem className="flex items-center gap-2">
|
{items.map((item) => (
|
||||||
<SidebarMenuButton
|
<Collapsible className="group/collapsible" defaultOpen={item.isActive} key={item.title}>
|
||||||
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"
|
<SidebarMenuItem>
|
||||||
tooltip="Quick Create"
|
<CollapsibleTrigger render={<SidebarMenuButton tooltip={item.title} />}>
|
||||||
>
|
{item.icon && <item.icon />}
|
||||||
<PlusCircleIcon />
|
<span>{item.title}</span>
|
||||||
<span>Quick Create</span>
|
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
</SidebarMenuButton>
|
</CollapsibleTrigger>
|
||||||
</SidebarMenuItem>
|
<CollapsibleContent>
|
||||||
</SidebarMenu>
|
<SidebarMenuSub>
|
||||||
<SidebarMenu>
|
{item.items?.map((subItem) => (
|
||||||
{items.map((item) => (
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
<Collapsible asChild className="group/collapsible" defaultOpen={true} key={item.title}>
|
<SidebarMenuSubButton
|
||||||
<SidebarMenuItem className="mb-6">
|
render={
|
||||||
<CollapsibleTrigger>
|
<a href={subItem.url} title={subItem.title}>
|
||||||
<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}>
|
|
||||||
<span>{subItem.title}</span>
|
<span>{subItem.title}</span>
|
||||||
</a>
|
</a>
|
||||||
</SidebarMenuSubButton>
|
}
|
||||||
</SidebarMenuSubItem>
|
/>
|
||||||
))}
|
</SidebarMenuSubItem>
|
||||||
</SidebarMenuSub>
|
))}
|
||||||
</CollapsibleContent>
|
</SidebarMenuSub>
|
||||||
</SidebarMenuItem>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</SidebarMenuItem>
|
||||||
))}
|
</Collapsible>
|
||||||
</SidebarMenu>
|
))}
|
||||||
</SidebarGroupContent>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</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,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@repo/shadcn-ui/components/sidebar";
|
} from "@repo/shadcn-ui/components/sidebar";
|
||||||
import {
|
import { BadgeCheck, Bell, ChevronsUpDownIcon, CreditCard, LogOut, Sparkles } from "lucide-react";
|
||||||
BellIcon,
|
|
||||||
CreditCardIcon,
|
|
||||||
LogOutIcon,
|
|
||||||
MoreVerticalIcon,
|
|
||||||
UserCircleIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user,
|
user,
|
||||||
@ -34,63 +28,72 @@ export function NavUser({
|
|||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger
|
||||||
<SidebarMenuButton
|
render={
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
<SidebarMenuButton
|
||||||
size="lg"
|
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} />
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<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">
|
|
||||||
<AvatarImage alt={user.name} src={user.avatar} />
|
<AvatarImage alt={user.name} src={user.avatar} />
|
||||||
<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-medium">{user.name}</span>
|
<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>
|
||||||
</div>
|
<ChevronsUpDownIcon className="ml-auto size-4" />
|
||||||
</DropdownMenuLabel>
|
</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 />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<UserCircleIcon />
|
<BadgeCheck />
|
||||||
Account
|
Account
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<CreditCardIcon />
|
<CreditCard />
|
||||||
Billing
|
Billing
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<BellIcon />
|
<Bell />
|
||||||
Notifications
|
Notifications
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<LogOutIcon />
|
<LogOut />
|
||||||
Log out
|
Log out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</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 {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
@ -36,48 +37,53 @@ export function TeamSwitcher({
|
|||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger
|
||||||
<SidebarMenuButton
|
render={
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
<SidebarMenuButton
|
||||||
size="lg"
|
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 className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||||
</div>
|
<activeTeam.logo className="size-4" />
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
</div>
|
||||||
<span className="truncate font-medium">{activeTeam.name}</span>
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate text-xs">{activeTeam.plan}</span>
|
<span className="truncate font-medium">{activeTeam.name}</span>
|
||||||
</div>
|
<span className="truncate text-xs">{activeTeam.plan}</span>
|
||||||
<ChevronsUpDown className="ml-auto" />
|
</div>
|
||||||
</SidebarMenuButton>
|
<ChevronsUpDown className="ml-auto" />
|
||||||
</DropdownMenuTrigger>
|
</SidebarMenuButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="start"
|
align="start"
|
||||||
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"}
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
|
<DropdownMenuGroup>
|
||||||
{teams.map((team, index) => (
|
<DropdownMenuLabel className="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
{teams.map((team, index) => (
|
||||||
className="gap-2 p-2"
|
<DropdownMenuItem
|
||||||
key={team.name}
|
className="gap-2 p-2"
|
||||||
onClick={() => setActiveTeam(team)}
|
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 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>
|
</div>
|
||||||
{team.name}
|
<div className="text-muted-foreground font-medium">Add team</div>
|
||||||
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
</DropdownMenuGroup>
|
||||||
<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>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user