feat(usage): refactor dashboard frontend with shadcn/ui components, skeleton states, and dark-theme charts
This commit is contained in:
1 parent
425b0009b8
commit
2f42d70d78
8 files changed
+555
-229
No files matched your search
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AlertTriangle, BarChart3, RotateCcw } from 'lucide-react';
|
||||
|
||||
interface ChartStatesProps {
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
isEmpty: boolean;
|
||||
onRetry: () => void;
|
||||
title: string;
|
||||
emptyTitle: string;
|
||||
emptyDescription: string;
|
||||
errorMessage: string;
|
||||
retryLabel: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ChartCard: React.FC<ChartStatesProps> = ({
|
||||
isLoading,
|
||||
error,
|
||||
isEmpty,
|
||||
onRetry,
|
||||
title,
|
||||
emptyTitle,
|
||||
emptyDescription,
|
||||
errorMessage,
|
||||
retryLabel,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Card className="border-border/60 bg-card/60 flex flex-col">
|
||||
<CardContent className="flex flex-1 flex-col p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-foreground text-sm font-semibold tracking-tight">{title}</h3>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{isLoading ? (
|
||||
<div className="flex h-[280px] flex-col gap-3">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex h-[280px] flex-col items-center justify-center gap-3">
|
||||
<AlertTriangle className="text-destructive h-5 w-5" />
|
||||
<p className="text-muted-foreground text-sm">{errorMessage}</p>
|
||||
<Button variant="outline" size="sm" onClick={onRetry}>
|
||||
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
||||
{retryLabel}
|
||||
</Button>
|
||||
</div>
|
||||
) : isEmpty ? (
|
||||
<div className="flex h-[280px] flex-col items-center justify-center gap-2">
|
||||
<BarChart3 className="text-muted-foreground/50 h-6 w-6" />
|
||||
<p className="text-foreground text-sm font-medium">{emptyTitle}</p>
|
||||
<p className="text-muted-foreground max-w-[240px] text-center text-xs">
|
||||
{emptyDescription}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TooltipPayloadItem {
|
||||
name?: string;
|
||||
value?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface CustomTooltipProps {
|
||||
active?: boolean;
|
||||
payload?: TooltipPayloadItem[];
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const CustomTooltip: React.FC<CustomTooltipProps> = ({ active, payload, label }) => {
|
||||
if (!active || !payload || payload.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-popover border-border rounded-md border px-3 py-2 shadow-md">
|
||||
<p className="text-foreground mb-1.5 text-xs font-medium">{label}</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
{payload.map((entry, index) => (
|
||||
<div key={index} className="flex items-center gap-2 text-xs">
|
||||
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: entry.color }} />
|
||||
<span className="text-muted-foreground">{entry.name}:</span>
|
||||
<span className="text-foreground font-mono font-medium">
|
||||
{typeof entry.value === 'number' ? entry.value.toLocaleString() : entry.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: number;
|
||||
icon: LucideIcon;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
accent?: 'blue' | 'green' | 'amber' | 'slate';
|
||||
}
|
||||
|
||||
const ACCENT_MAP = {
|
||||
blue: 'text-blue-400',
|
||||
green: 'text-emerald-400',
|
||||
amber: 'text-amber-400',
|
||||
slate: 'text-slate-400',
|
||||
};
|
||||
|
||||
const BG_MAP = {
|
||||
blue: 'bg-blue-500/5',
|
||||
green: 'bg-emerald-500/5',
|
||||
amber: 'bg-amber-500/5',
|
||||
slate: 'bg-slate-500/5',
|
||||
};
|
||||
|
||||
export const StatCard: React.FC<StatCardProps> = ({
|
||||
label,
|
||||
value,
|
||||
icon: Icon,
|
||||
isLoading,
|
||||
className,
|
||||
accent = 'slate',
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
'border-border/60 bg-card/60 hover:bg-card backdrop-blur-none transition-colors',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||
{label}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-7 w-24" />
|
||||
) : (
|
||||
<span className="text-foreground font-mono text-2xl font-semibold tracking-tight">
|
||||
{value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-9 w-9 shrink-0 items-center justify-center rounded-md',
|
||||
BG_MAP[accent],
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('h-4 w-4', ACCENT_MAP[accent])} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user