mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
133 lines
4 KiB
TypeScript
133 lines
4 KiB
TypeScript
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;
|