MotorSignal-App/website/components/founder-portal/metric-card.tsx

199 lines
7.5 KiB
TypeScript

'use client';
import React from 'react';
import { ClubMetrics } from './types';
interface MetricCardProps {
label: string;
value: string | number;
unit?: string;
badge?: React.ReactNode;
subtextLeft?: React.ReactNode;
subtextRight?: React.ReactNode;
disclaimer?: string;
sparklineVariant?: 'secondary' | 'error' | 'primary';
showSparkline?: boolean;
isUnavailable?: boolean;
}
export function MetricCard({
label,
value,
unit,
badge,
subtextLeft,
subtextRight,
disclaimer,
sparklineVariant = 'secondary',
showSparkline = false,
isUnavailable = false,
}: MetricCardProps) {
const sparklineColors = {
secondary: { stroke: 'text-secondary', fill: '#38bdf8', path: 'M0 24 L25 18 L50 20 L75 10 L100 4' },
error: { stroke: 'text-error', fill: '#f87171', path: 'M0 15 L25 18 L50 10 L75 22 L100 8' },
primary: { stroke: 'text-primary', fill: '#0088ff', path: 'M0 26 L30 20 L60 14 L100 8' },
};
const selectedSpark = sparklineColors[sparklineVariant] || sparklineColors.secondary;
return (
<div className="relative rounded-2xl glass-card border border-brand-border/60 p-4 sm:p-5 flex flex-col justify-between transition-all hover:border-brand-cyan/50 hover:shadow-[0_0_20px_rgba(0,242,254,0.12)] overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-brand-cyan/50 via-transparent to-transparent" />
{/* Top Header Row */}
<div className="flex items-start justify-between gap-2 flex-wrap">
<span className="font-mono text-xs tracking-wider text-slate-400 font-medium uppercase">{label}</span>
{badge}
</div>
{/* Primary Numerical Value & Sparkline */}
<div className="my-3 flex items-baseline justify-between gap-2">
<div className="flex items-baseline gap-1.5">
<span className="font-headline text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
{isUnavailable ? '--' : value}
</span>
{unit && !isUnavailable && (
<span className="font-mono text-xs text-brand-cyan font-bold">{unit}</span>
)}
</div>
{showSparkline && !isUnavailable && (
<div className="w-20 h-6 shrink-0" aria-hidden="true">
<svg className={`w-full h-full ${selectedSpark.stroke}`} fill="none" viewBox="0 0 100 30">
<path
d={selectedSpark.path}
stroke="currentColor"
strokeLinecap="round"
strokeWidth="2"
/>
<path
d={`${selectedSpark.path} L100 30 L0 30 Z`}
fill="currentColor"
fillOpacity="0.08"
/>
</svg>
</div>
)}
</div>
{/* Footer Subtext */}
<div className="flex flex-col gap-1 w-full pt-2 border-t border-brand-border/40">
<div className="flex flex-col gap-1 font-mono text-xs text-slate-400">
{subtextLeft && <div className="leading-snug">{subtextLeft}</div>}
{subtextRight && <div className="text-slate-400 leading-snug">{subtextRight}</div>}
</div>
{disclaimer && (
<span className="text-[10px] font-mono text-slate-500 line-clamp-2 leading-relaxed" title={disclaimer}>
{disclaimer}
</span>
)}
</div>
</div>
);
}
export function ExecutiveMetrics({ metrics }: { metrics: ClubMetrics }) {
return (
<section aria-label="Executive Metrics" className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 2xl:grid-cols-4 gap-4 lg:gap-5">
{/* 1. MEMBERS */}
<MetricCard
label="Members"
value={metrics.members.toLocaleString()}
showSparkline={false}
badge={
metrics.membersActivePercent != null ? (
<span className="inline-flex items-center gap-1 font-mono text-[11px] text-brand-cyan bg-brand-cyan/10 px-2 py-0.5 rounded-full border border-brand-cyan/30 shrink-0">
<span className="w-1.5 h-1.5 rounded-full bg-brand-cyan" />
{metrics.membersActivePercent}% ACTIVE
</span>
) : (
<span className="font-mono text-[10px] text-slate-400 bg-surface-container px-2 py-0.5 rounded-full border border-brand-border/60 shrink-0">ROSTER</span>
)
}
subtextLeft={
metrics.membersDeltaMonth != null ? (
<span className="text-brand-cyan font-medium">
+{metrics.membersDeltaMonth} this month
</span>
) : (
<span>Total Roster</span>
)
}
subtextRight={
metrics.roadMarshalsCount != null ? (
<span>{metrics.roadMarshalsCount} Road Marshals</span>
) : (
<span>Active members</span>
)
}
/>
{/* 2. PENDING APPLICATIONS */}
<MetricCard
label="Pending Applications"
value={metrics.pendingApplications}
showSparkline={false}
badge={
metrics.pendingApplications > 0 ? (
<span className="px-2 py-0.5 rounded-full bg-error-container text-on-error-container font-mono text-[10px] font-semibold border border-error/30 shrink-0">
REQUIRES REVIEW
</span>
) : (
<span className="font-mono text-[10px] text-slate-400 bg-surface-container px-2 py-0.5 rounded-full border border-brand-border/60 shrink-0">ALL CLEARED</span>
)
}
subtextLeft={
metrics.pendingApplications > 0 ? (
<span className="text-error font-medium">Requires review</span>
) : (
<span>No pending</span>
)
}
subtextRight={<span>Awaiting founder review</span>}
/>
{/* 3. UPCOMING RIDES */}
<MetricCard
label="Upcoming Rides"
value={metrics.upcomingRidesCount}
showSparkline={false}
badge={
<span className="font-mono text-[11px] text-brand-cyan bg-brand-cyan/10 px-2 py-0.5 rounded-full border border-brand-cyan/30 font-medium shrink-0">
{metrics.upcomingRidesCount > 0 ? 'SCHEDULED' : 'NONE'}
</span>
}
subtextLeft={
<span className="text-on-surface font-medium">
{metrics.confirmedRidersTotal} riders confirmed
</span>
}
subtextRight={<span>Next: {metrics.nextRideDate ?? 'None'}</span>}
/>
{/* 4. CLUB MILES THIS MONTH */}
<MetricCard
label="Club Miles (This Month)"
value={metrics.clubMilesThisMonth ? metrics.clubMilesThisMonth.toLocaleString() : '--'}
unit="MI"
showSparkline={false}
isUnavailable={!metrics.clubMilesAvailable}
badge={
metrics.clubMilesAvailable ? (
<span className="font-mono text-[10px] text-brand-cyan bg-brand-cyan/10 px-2 py-0.5 rounded-full border border-brand-cyan/30 font-medium max-w-full text-right leading-tight">
Completed Club Rides only
</span>
) : (
<span className="font-mono text-[10px] text-slate-400 bg-surface-container px-2 py-0.5 rounded-full border border-brand-border/60 shrink-0">UNAVAILABLE</span>
)
}
subtextLeft={
<span className="text-slate-300 font-medium">
{metrics.completedClubRidesCount} completed Club Rides
</span>
}
subtextRight={<span className="text-slate-400">Eligible rides only</span>}
disclaimer="Calculated from eligible completed Club Rides. Private rider mileage is not automatically included."
/>
</section>
);
}