120 lines
4.2 KiB
TypeScript
120 lines
4.2 KiB
TypeScript
import React, { useMemo } from "react";
|
|
import { Users, CalendarDays, BarChart2, Trophy } from "lucide-react";
|
|
import { useBreakpoint } from "../../hooks/useBreakpoint";
|
|
|
|
function fmt(n: number | undefined): string {
|
|
if (n == null || Number.isNaN(n)) return "—";
|
|
return n.toLocaleString();
|
|
}
|
|
|
|
export function StatsBar({
|
|
loading,
|
|
participantCount,
|
|
sessionCount,
|
|
quizPollCount,
|
|
pointsAwarded,
|
|
statusCounts,
|
|
upcomingSessionCount,
|
|
pointsRedeemed,
|
|
}: {
|
|
loading?: boolean;
|
|
participantCount?: number;
|
|
sessionCount?: number;
|
|
quizPollCount?: number;
|
|
pointsAwarded?: number;
|
|
pointsRedeemed?: number;
|
|
statusCounts?: Record<string, number>;
|
|
upcomingSessionCount?: number;
|
|
}) {
|
|
const bp = useBreakpoint();
|
|
const cols = bp === "mobile" ? 2 : 4;
|
|
|
|
const participantTrend = useMemo(() => {
|
|
if (loading) return undefined;
|
|
const ci = statusCounts?.checked_in ?? 0;
|
|
const conf = statusCounts?.confirmed ?? 0;
|
|
if (ci) return `${ci.toLocaleString()} checked in`;
|
|
if (conf) return `${conf.toLocaleString()} confirmed`;
|
|
return undefined;
|
|
}, [loading, statusCounts]);
|
|
|
|
const sessionTrend =
|
|
!loading && upcomingSessionCount != null && upcomingSessionCount > 0
|
|
? `${upcomingSessionCount} upcoming`
|
|
: undefined;
|
|
|
|
const pointsTrend =
|
|
!loading && pointsRedeemed != null && pointsRedeemed > 0
|
|
? `${pointsRedeemed.toLocaleString()} redeemed`
|
|
: undefined;
|
|
|
|
const stats = [
|
|
{
|
|
icon: <Users size={15} />,
|
|
label: "Participants",
|
|
value: loading ? "—" : fmt(participantCount),
|
|
trend: participantTrend,
|
|
trendUp: !!(statusCounts?.checked_in && statusCounts.checked_in > 0),
|
|
accent: "var(--chart-1)",
|
|
},
|
|
{
|
|
icon: <CalendarDays size={15} />,
|
|
label: "Sessions",
|
|
value: loading ? "—" : fmt(sessionCount),
|
|
trend: sessionTrend,
|
|
trendUp: false,
|
|
accent: "var(--chart-3)",
|
|
},
|
|
{
|
|
icon: <BarChart2 size={15} />,
|
|
label: "Quizzes & Polls",
|
|
value: loading ? "—" : fmt(quizPollCount),
|
|
trend: undefined,
|
|
trendUp: false,
|
|
accent: "var(--chart-4)",
|
|
},
|
|
{
|
|
icon: <Trophy size={15} />,
|
|
label: "Points Awarded",
|
|
value: loading ? "—" : fmt(pointsAwarded),
|
|
trend: pointsTrend,
|
|
trendUp: false,
|
|
accent: "var(--chart-2)",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div style={{ display: "grid", gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: bp === "mobile" ? "10px" : "12px" }}>
|
|
{stats.map((stat, idx) => (
|
|
<div key={idx} style={{
|
|
background: "var(--card)",
|
|
borderTopWidth: "1px", borderRightWidth: "1px",
|
|
borderBottomWidth: "1px", borderLeftWidth: "3px",
|
|
borderTopStyle: "solid", borderRightStyle: "solid",
|
|
borderBottomStyle: "solid", borderLeftStyle: "solid",
|
|
borderTopColor: "var(--border)", borderRightColor: "var(--border)",
|
|
borderBottomColor: "var(--border)", borderLeftColor: stat.accent,
|
|
borderRadius: "var(--radius-card)", padding: bp === "mobile" ? "14px 14px" : "16px 20px",
|
|
display: "flex", alignItems: "flex-start", gap: "12px",
|
|
}}>
|
|
<div style={{ width: "30px", height: "30px", borderRadius: "var(--radius-button)", background: stat.accent + "15", display: "flex", alignItems: "center", justifyContent: "center", color: stat.accent, flexShrink: 0, marginTop: "2px" }}>
|
|
{stat.icon}
|
|
</div>
|
|
<div>
|
|
<p style={{ fontSize: bp === "mobile" ? "var(--text-xl)" : "var(--text-2xl)", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", color: "var(--foreground)", lineHeight: "1.1" }}>
|
|
{stat.value}
|
|
</p>
|
|
<p style={{ fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginTop: "3px", lineHeight: "1.3" }}>
|
|
{stat.label}
|
|
</p>
|
|
{stat.trend && (
|
|
<p style={{ fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: stat.trendUp ? "#16a34a" : "var(--muted)", marginTop: "6px", lineHeight: "1.2" }}>
|
|
{stat.trend}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>);
|
|
}
|