Remove Tailwind CSS configuration file

This commit is contained in:
Max Kowalski
2025-06-28 00:23:23 +02:00
parent a6632d6dfc
commit 1be9ce9dd9
42 changed files with 3475 additions and 1028 deletions

View File

@ -21,9 +21,9 @@ export default function DateRangePicker({
const [endDate, setEndDate] = useState(initialEndDate || maxDate);
useEffect(() => {
// Notify parent component when dates change
// Only notify parent component when dates change, not when the callback changes
onDateRangeChange(startDate, endDate);
}, [startDate, endDate, onDateRangeChange]);
}, [startDate, endDate]);
const handleStartDateChange = (newStartDate: string) => {
// Ensure start date is not before min date

View File

@ -1,10 +1,15 @@
"use client";
import { useRef, useEffect } from "react";
import Chart from "chart.js/auto";
import annotationPlugin from "chartjs-plugin-annotation";
Chart.register(annotationPlugin);
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
ReferenceLine,
} from "recharts";
interface ResponseTimeDistributionProps {
data: number[];
@ -12,114 +17,145 @@ interface ResponseTimeDistributionProps {
targetResponseTime?: number;
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{payload[0].value}
</span>{" "}
responses
</p>
</div>
);
}
return null;
};
export default function ResponseTimeDistribution({
data,
average,
targetResponseTime,
}: ResponseTimeDistributionProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
if (!data || !data.length) {
return (
<div className="flex items-center justify-center h-64 text-muted-foreground">
No response time data available
</div>
);
}
useEffect(() => {
if (!ref.current || !data || !data.length) return;
// Create bins for the histogram (0-1s, 1-2s, 2-3s, etc.)
const maxTime = Math.ceil(Math.max(...data));
const bins = Array(Math.min(maxTime + 1, 10)).fill(0);
const ctx = ref.current.getContext("2d");
if (!ctx) return;
// Count responses in each bin
data.forEach((time) => {
const binIndex = Math.min(Math.floor(time), bins.length - 1);
bins[binIndex]++;
});
// Create bins for the histogram (0-1s, 1-2s, 2-3s, etc.)
const maxTime = Math.ceil(Math.max(...data));
const bins = Array(Math.min(maxTime + 1, 10)).fill(0);
// Create chart data
const chartData = bins.map((count, i) => {
let label;
if (i === bins.length - 1 && bins.length < maxTime + 1) {
label = `${i}+ sec`;
} else {
label = `${i}-${i + 1} sec`;
}
// Count responses in each bin
data.forEach((time) => {
const binIndex = Math.min(Math.floor(time), bins.length - 1);
bins[binIndex]++;
});
// Determine color based on response time
let color;
if (i <= 2) color = "hsl(var(--chart-1))"; // Green for fast
else if (i <= 5) color = "hsl(var(--chart-4))"; // Yellow for medium
else color = "hsl(var(--chart-3))"; // Red for slow
// Create labels for each bin
const labels = bins.map((_, i) => {
if (i === bins.length - 1 && bins.length < maxTime + 1) {
return `${i}+ seconds`;
}
return `${i}-${i + 1} seconds`;
});
return {
name: label,
value: count,
color,
};
});
const chart = new Chart(ctx, {
type: "bar",
data: {
labels,
datasets: [
{
label: "Responses",
data: bins,
backgroundColor: bins.map((_, i) => {
// Green for fast, yellow for medium, red for slow
if (i <= 2) return "rgba(34, 197, 94, 0.7)"; // Green
if (i <= 5) return "rgba(250, 204, 21, 0.7)"; // Yellow
return "rgba(239, 68, 68, 0.7)"; // Red
}),
borderWidth: 1,
},
],
},
options: {
responsive: true,
plugins: {
legend: { display: false },
annotation: {
annotations: {
averageLine: {
type: "line",
yMin: 0,
yMax: Math.max(...bins),
xMin: average,
xMax: average,
borderColor: "rgba(75, 192, 192, 1)",
borderWidth: 2,
label: {
display: true,
content: "Avg: " + average.toFixed(1) + "s",
position: "start",
},
},
targetLine: targetResponseTime
? {
type: "line",
yMin: 0,
yMax: Math.max(...bins),
xMin: targetResponseTime,
xMax: targetResponseTime,
borderColor: "rgba(75, 192, 192, 0.7)",
borderWidth: 2,
label: {
display: true,
content: "Target",
position: "end",
},
}
: undefined,
},
},
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: "Number of Responses",
},
},
x: {
title: {
display: true,
text: "Response Time",
},
},
},
},
});
return (
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
strokeOpacity={0.3}
/>
<XAxis
dataKey="name"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
label={{
value: 'Number of Responses',
angle: -90,
position: 'insideLeft',
style: { textAnchor: 'middle' }
}}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey="value"
radius={[4, 4, 0, 0]}
fill="hsl(var(--chart-1))"
>
{chartData.map((entry, index) => (
<Bar key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
return () => chart.destroy();
}, [data, average, targetResponseTime]);
{/* Average line */}
<ReferenceLine
x={Math.floor(average)}
stroke="hsl(var(--primary))"
strokeWidth={2}
strokeDasharray="5 5"
label={{
value: `Avg: ${average.toFixed(1)}s`,
position: "top" as const,
style: {
fill: "hsl(var(--primary))",
fontSize: "12px",
fontWeight: "500"
}
}}
/>
return <canvas ref={ref} height={180} />;
{/* Target line (if provided) */}
{targetResponseTime && (
<ReferenceLine
x={Math.floor(targetResponseTime)}
stroke="hsl(var(--chart-2))"
strokeWidth={2}
strokeDasharray="3 3"
label={{
value: `Target: ${targetResponseTime}s`,
position: "top" as const,
style: {
fill: "hsl(var(--chart-2))",
fontSize: "12px",
fontWeight: "500"
}
}}
/>
)}
</BarChart>
</ResponsiveContainer>
</div>
);
}

View File

@ -0,0 +1,105 @@
"use client";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface BarChartProps {
data: Array<{ name: string; value: number; [key: string]: any }>;
title?: string;
dataKey?: string;
colors?: string[];
height?: number;
className?: string;
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{payload[0].value}
</span>{" "}
sessions
</p>
</div>
);
}
return null;
};
export default function ModernBarChart({
data,
title,
dataKey = "value",
colors = [
"hsl(var(--chart-1))",
"hsl(var(--chart-2))",
"hsl(var(--chart-3))",
"hsl(var(--chart-4))",
"hsl(var(--chart-5))",
],
height = 300,
className,
}: BarChartProps) {
return (
<Card className={className}>
{title && (
<CardHeader>
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
</CardHeader>
)}
<CardContent>
<ResponsiveContainer width="100%" height={height}>
<BarChart data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
strokeOpacity={0.3}
/>
<XAxis
dataKey="name"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
angle={-45}
textAnchor="end"
height={80}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey={dataKey}
radius={[4, 4, 0, 0]}
className="transition-all duration-200"
>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={colors[index % colors.length]}
className="hover:opacity-80"
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,122 @@
"use client";
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface DonutChartProps {
data: Array<{ name: string; value: number; color?: string }>;
title?: string;
centerText?: {
title: string;
value: string | number;
};
colors?: string[];
height?: number;
className?: string;
}
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0];
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{data.name}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{data.value}
</span>{" "}
sessions ({((data.value / data.payload.total) * 100).toFixed(1)}%)
</p>
</div>
);
}
return null;
};
const CustomLegend = ({ payload }: any) => {
return (
<div className="flex flex-wrap justify-center gap-4 mt-4">
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-sm text-muted-foreground">{entry.value}</span>
</div>
))}
</div>
);
};
const CenterLabel = ({ centerText, total }: any) => {
if (!centerText) return null;
return (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="text-center">
<p className="text-2xl font-bold">{centerText.value}</p>
<p className="text-sm text-muted-foreground">{centerText.title}</p>
</div>
</div>
);
};
export default function ModernDonutChart({
data,
title,
centerText,
colors = [
"hsl(var(--chart-1))",
"hsl(var(--chart-2))",
"hsl(var(--chart-3))",
"hsl(var(--chart-4))",
"hsl(var(--chart-5))",
],
height = 300,
className,
}: DonutChartProps) {
const total = data.reduce((sum, item) => sum + item.value, 0);
const dataWithTotal = data.map(item => ({ ...item, total }));
return (
<Card className={className}>
{title && (
<CardHeader>
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
</CardHeader>
)}
<CardContent>
<div className="relative">
<ResponsiveContainer width="100%" height={height}>
<PieChart>
<Pie
data={dataWithTotal}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={2}
dataKey="value"
className="transition-all duration-200"
>
{dataWithTotal.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color || colors[index % colors.length]}
className="hover:opacity-80 cursor-pointer"
stroke="hsl(var(--background))"
strokeWidth={2}
/>
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
<Legend content={<CustomLegend />} />
</PieChart>
</ResponsiveContainer>
<CenterLabel centerText={centerText} total={total} />
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,117 @@
"use client";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Area,
AreaChart,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface LineChartProps {
data: Array<{ date: string; value: number; [key: string]: any }>;
title?: string;
dataKey?: string;
color?: string;
gradient?: boolean;
height?: number;
className?: string;
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="text-sm font-medium">{label}</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{payload[0].value}
</span>{" "}
sessions
</p>
</div>
);
}
return null;
};
export default function ModernLineChart({
data,
title,
dataKey = "value",
color = "hsl(var(--primary))",
gradient = true,
height = 300,
className,
}: LineChartProps) {
const ChartComponent = gradient ? AreaChart : LineChart;
return (
<Card className={className}>
{title && (
<CardHeader>
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
</CardHeader>
)}
<CardContent>
<ResponsiveContainer width="100%" height={height}>
<ChartComponent data={data} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<defs>
{gradient && (
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.3} />
<stop offset="95%" stopColor={color} stopOpacity={0.05} />
</linearGradient>
)}
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
strokeOpacity={0.3}
/>
<XAxis
dataKey="date"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
{gradient ? (
<Area
type="monotone"
dataKey={dataKey}
stroke={color}
strokeWidth={2}
fill="url(#colorGradient)"
dot={{ fill: color, strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: color, strokeWidth: 2 }}
/>
) : (
<Line
type="monotone"
dataKey={dataKey}
stroke={color}
strokeWidth={2}
dot={{ fill: color, strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: color, strokeWidth: 2 }}
/>
)}
</ChartComponent>
</ResponsiveContainer>
</CardContent>
</Card>
);
}

46
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

59
components/ui/button.tsx Normal file
View File

@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-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",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

92
components/ui/card.tsx Normal file
View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@ -0,0 +1,163 @@
"use client";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { TrendingUp, TrendingDown, Minus } from "lucide-react";
interface MetricCardProps {
title: string;
value: string | number | null | undefined;
description?: string;
icon?: React.ReactNode;
trend?: {
value: number;
label?: string;
isPositive?: boolean;
};
variant?: "default" | "primary" | "success" | "warning" | "danger";
isLoading?: boolean;
className?: string;
}
export default function MetricCard({
title,
value,
description,
icon,
trend,
variant = "default",
isLoading = false,
className,
}: MetricCardProps) {
if (isLoading) {
return (
<Card className={cn("relative overflow-hidden", className)}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-10 rounded-full" />
</div>
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16 mb-2" />
<Skeleton className="h-3 w-20" />
</CardContent>
</Card>
);
}
const getVariantClasses = () => {
switch (variant) {
case "primary":
return "border-primary/20 bg-gradient-to-br from-primary/5 to-primary/10";
case "success":
return "border-green-200 bg-gradient-to-br from-green-50 to-green-100 dark:border-green-800 dark:from-green-950 dark:to-green-900";
case "warning":
return "border-amber-200 bg-gradient-to-br from-amber-50 to-amber-100 dark:border-amber-800 dark:from-amber-950 dark:to-amber-900";
case "danger":
return "border-red-200 bg-gradient-to-br from-red-50 to-red-100 dark:border-red-800 dark:from-red-950 dark:to-red-900";
default:
return "border-border bg-gradient-to-br from-card to-muted/20";
}
};
const getIconClasses = () => {
switch (variant) {
case "primary":
return "bg-primary/10 text-primary border-primary/20";
case "success":
return "bg-green-100 text-green-600 border-green-200 dark:bg-green-900 dark:text-green-400 dark:border-green-800";
case "warning":
return "bg-amber-100 text-amber-600 border-amber-200 dark:bg-amber-900 dark:text-amber-400 dark:border-amber-800";
case "danger":
return "bg-red-100 text-red-600 border-red-200 dark:bg-red-900 dark:text-red-400 dark:border-red-800";
default:
return "bg-muted text-muted-foreground border-border";
}
};
const getTrendIcon = () => {
if (!trend) return null;
if (trend.value === 0) {
return <Minus className="h-3 w-3" />;
}
return trend.isPositive !== false ? (
<TrendingUp className="h-3 w-3" />
) : (
<TrendingDown className="h-3 w-3" />
);
};
const getTrendColor = () => {
if (!trend || trend.value === 0) return "text-muted-foreground";
return trend.isPositive !== false ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400";
};
return (
<Card
className={cn(
"relative overflow-hidden transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5",
getVariantClasses(),
className
)}
>
{/* Subtle gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-white/50 to-transparent dark:from-white/5 pointer-events-none" />
<CardHeader className="pb-3 relative">
<div className="flex items-start justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground leading-none">
{title}
</p>
{description && (
<p className="text-xs text-muted-foreground/80">
{description}
</p>
)}
</div>
{icon && (
<div
className={cn(
"flex h-10 w-10 shrink-0 items-center justify-center rounded-full border transition-colors",
getIconClasses()
)}
>
<span className="text-lg">{icon}</span>
</div>
)}
</div>
</CardHeader>
<CardContent className="relative">
<div className="flex items-end justify-between">
<div className="space-y-1">
<p className="text-2xl font-bold tracking-tight">
{value ?? "—"}
</p>
{trend && (
<Badge
variant="secondary"
className={cn(
"text-xs font-medium px-2 py-0.5 gap-1",
getTrendColor(),
"bg-background/50 border-current/20"
)}
>
{getTrendIcon()}
{Math.abs(trend.value).toFixed(1)}%
{trend.label && ` ${trend.label}`}
</Badge>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }

61
components/ui/tooltip.tsx Normal file
View File

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }