Kuma/src/modules/dashboard/ui/components/DashboardStatsGrid.tsx

133 lines
4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { TrendingUp, TrendingDown, Minus, Users, CalendarDays, Dumbbell } from "lucide-react";
import type { DashboardStats } from "../../actions/dashboard-actions";
function formatEur(cents: number) {
return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(cents / 100);
}
function ChangeChip({ current, previous }: { current: number; previous: number }) {
if (previous === 0 && current === 0) {
return (
<span className="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-gray-100 text-gray-500">
<Minus className="size-3" /> –
</span>
);
}
if (previous === 0) {
return (
<span className="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-green-100 text-green-700">
<TrendingUp className="size-3" /> Neu
</span>
);
}
const pct = ((current - previous) / previous) * 100;
const up = pct >= 0;
return (
<span
className={`inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full ${
up ? "bg-green-100 text-green-700" : "bg-red-100 text-red-600"
}`}
>
{up ? <TrendingUp className="size-3" /> : <TrendingDown className="size-3" />}
{up ? "+" : ""}
{Math.abs(pct).toFixed(1)} %
</span>
);
}
interface StatTileProps {
label: string;
subLabel: string;
value: string;
current: number;
previous: number;
icon: React.ComponentType<{ className?: string }>;
accent?: string;
}
function StatTile({
label,
subLabel,
value,
current,
previous,
icon: Icon,
accent,
}: StatTileProps) {
return (
<div className="border border-white/60 bg-white/60 backdrop-blur-xl rounded-2xl shadow-sm p-5 relative overflow-hidden">
<div
className="absolute inset-0 pointer-events-none opacity-40"
style={{
background: accent
? `radial-gradient(ellipse 80% 80% at 110% -10%, ${accent} 0%, transparent 70%)`
: undefined,
}}
/>
<div className="flex items-start justify-between mb-3 relative">
<div className="flex items-center gap-2 text-gray-500">
<Icon className="size-3.5" />
<span className="text-xs font-semibold tracking-wide uppercase">{label}</span>
</div>
<ChangeChip current={current} previous={previous} />
</div>
<p className="text-3xl font-black text-gray-900 relative">{value}</p>
<p className="text-xs text-gray-400 mt-1 relative">{subLabel}</p>
</div>
);
}
interface DashboardStatsGridProps {
stats: DashboardStats;
}
const DashboardStatsGrid = ({ stats }: DashboardStatsGridProps) => {
const tiles: StatTileProps[] = [
{
label: "Kurse",
subLabel: "Kurse heute · vs. gestern",
value: String(stats.coursesToday),
current: stats.coursesToday,
previous: stats.coursesTodayYesterday,
icon: Dumbbell,
accent: "oklch(0.88 0.09 85 / 0.5)",
},
{
label: "Teilnehmer",
subLabel: "Heute · vs. gestern",
value: String(stats.participantsToday),
current: stats.participantsToday,
previous: stats.participantsTodayYesterday,
icon: Users,
accent: "oklch(0.88 0.09 85 / 0.5)",
},
{
label: "Umsatz",
subLabel: "Diesen Monat · vs. letzten Monat",
value: formatEur(stats.revenueThisMonth),
current: stats.revenueThisMonth,
previous: stats.revenueLastMonth,
icon: TrendingUp,
accent: "oklch(0.82 0.06 240 / 0.4)",
},
{
label: "Kurse",
subLabel: "Diesen Monat · vs. letzten Monat",
value: String(stats.coursesThisMonth),
current: stats.coursesThisMonth,
previous: stats.coursesLastMonth,
icon: CalendarDays,
accent: "oklch(0.82 0.06 240 / 0.4)",
},
];
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{tiles.map((tile) => (
<StatTile key={tile.label + tile.subLabel} {...tile} />
))}
</div>
);
};
export default DashboardStatsGrid;