feat(admin): color-code dashboard Overview by category, unify depth system
The Overview card was a flat, uncategorized list of 10 stats with no visual grouping. Split it into three labeled, color-identified groups so a moderator can scan by kind of number instead of reading every label: - Activity (emerald): owners, pets, posts, DAU, MAU - Violations (rose): banned users, shadowbanned, blacklist entries — values only pick up the color when actually > 0, same "act on this" convention as the existing AttentionStat tiles - Info (sky): active ads, verifications closed Also switched the maintenance banner and AttentionStat tiles from border to ring-1, matching the shared Card component's depth convention so the whole dashboard uses one visual vocabulary instead of two.
This commit is contained in:
1 parent
f12f4edc69
commit
2f7cacfc82
1 file changed
+102
-30
+102
-30
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { Flag, Clock, TriangleAlert } from "lucide-react";
|
||||
import { Flag, Clock, TriangleAlert, TrendingUp, ShieldAlert, Info } from "lucide-react";
|
||||
import { TimeSeriesChart } from "@/components/admin/TimeSeriesChart";
|
||||
import { GranularityToggle, type Granularity } from "@/components/admin/GranularityToggle";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -56,10 +56,10 @@ export default function AdminDashboard() {
|
||||
|
||||
{canToggleMaintenance && (
|
||||
<div
|
||||
className={`flex items-center justify-between gap-4 rounded-xl border px-5 py-4 ${
|
||||
className={`flex items-center justify-between gap-4 rounded-xl ring-1 px-5 py-4 ${
|
||||
maintenance?.enabled
|
||||
? "border-destructive/40 bg-destructive/10"
|
||||
: "border-border bg-card"
|
||||
? "ring-destructive/40 bg-destructive/10"
|
||||
: "ring-foreground/10 bg-card"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -92,27 +92,60 @@ export default function AdminDashboard() {
|
||||
<>
|
||||
{/* Needs attention — the only stats that earn individual weight and
|
||||
real color; color means "act on this", not "posts are orange". */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<AttentionStat icon={Flag} label="Open reports" value={stats?.reports ?? 0} />
|
||||
<AttentionStat icon={Clock} label="Verifications pending" value={stats?.verificationPending ?? 0} />
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-foreground mb-3">Needs attention</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<AttentionStat icon={Flag} label="Open reports" value={stats?.reports ?? 0} />
|
||||
<AttentionStat icon={Clock} label="Verifications pending" value={stats?.verificationPending ?? 0} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Everything else — a flat overview, not a wall of identical cards */}
|
||||
<div className="rounded-xl border border-border bg-card p-5">
|
||||
<h2 className="text-sm font-semibold text-foreground mb-4">Overview</h2>
|
||||
<dl className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-4">
|
||||
<OverviewStat label="Owners" value={stats?.owners ?? 0} />
|
||||
<OverviewStat label="Pets" value={stats?.pets ?? 0} />
|
||||
<OverviewStat label="Posts" value={stats?.posts ?? 0} />
|
||||
<OverviewStat label="DAU (today)" value={stats?.dau ?? 0} />
|
||||
<OverviewStat label="MAU (30d)" value={stats?.mau ?? 0} />
|
||||
<OverviewStat label="Active ads" value={stats?.activeAds ?? 0} />
|
||||
<OverviewStat label="Banned users" value={stats?.bans ?? 0} />
|
||||
<OverviewStat label="Shadowbanned" value={stats?.shadowbans ?? 0} />
|
||||
<OverviewStat label="Blacklist entries" value={stats?.blacklistEntries ?? 0} />
|
||||
<OverviewStat label="Verifications closed" value={stats?.verificationClosed ?? 0} />
|
||||
</dl>
|
||||
</div>
|
||||
{/* Everything else, grouped by what kind of number it is — activity
|
||||
(growth/usage), violations (moderation risk), info (context only).
|
||||
Category color is a legend, not a wash: only violation values
|
||||
that are actually > 0 pick up the color, same "act on this"
|
||||
logic as AttentionStat above. */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Overview</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<StatGroup
|
||||
icon={TrendingUp}
|
||||
label="Activity"
|
||||
color="emerald"
|
||||
items={[
|
||||
{ label: "Owners", value: stats?.owners ?? 0 },
|
||||
{ label: "Pets", value: stats?.pets ?? 0 },
|
||||
{ label: "Posts", value: stats?.posts ?? 0 },
|
||||
{ label: "DAU (today)", value: stats?.dau ?? 0 },
|
||||
{ label: "MAU (30d)", value: stats?.mau ?? 0 },
|
||||
]}
|
||||
/>
|
||||
<StatGroup
|
||||
icon={ShieldAlert}
|
||||
label="Violations"
|
||||
color="rose"
|
||||
emphasizeNonZero
|
||||
items={[
|
||||
{ label: "Banned users", value: stats?.bans ?? 0 },
|
||||
{ label: "Shadowbanned", value: stats?.shadowbans ?? 0 },
|
||||
{ label: "Blacklist entries", value: stats?.blacklistEntries ?? 0 },
|
||||
]}
|
||||
/>
|
||||
<StatGroup
|
||||
icon={Info}
|
||||
label="Info"
|
||||
color="sky"
|
||||
items={[
|
||||
{ label: "Active ads", value: stats?.activeAds ?? 0 },
|
||||
{ label: "Verifications closed", value: stats?.verificationClosed ?? 0 },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -227,8 +260,8 @@ function AttentionStat({
|
||||
const needsAttention = value > 0;
|
||||
return (
|
||||
<div
|
||||
className={`rounded-xl border px-5 py-4 flex items-center gap-4 ${
|
||||
needsAttention ? "border-orange-500/30 bg-orange-500/5" : "border-border bg-card"
|
||||
className={`rounded-xl ring-1 px-5 py-4 flex items-center gap-4 ${
|
||||
needsAttention ? "ring-orange-500/30 bg-orange-500/5" : "ring-foreground/10 bg-card"
|
||||
}`}
|
||||
>
|
||||
<Icon className={`h-5 w-5 shrink-0 ${needsAttention ? "text-orange-400" : "text-muted-foreground"}`} />
|
||||
@@ -242,12 +275,51 @@ function AttentionStat({
|
||||
);
|
||||
}
|
||||
|
||||
/** Everything else — a plain label/value pair, no per-item card chrome. */
|
||||
function OverviewStat({ label, value }: { label: string; value: number }) {
|
||||
const STAT_GROUP_COLORS = {
|
||||
emerald: { text: "text-emerald-400", bg: "bg-emerald-500/5", ring: "ring-emerald-500/15" },
|
||||
rose: { text: "text-rose-400", bg: "bg-rose-500/5", ring: "ring-rose-500/15" },
|
||||
sky: { text: "text-sky-400", bg: "bg-sky-500/5", ring: "ring-sky-500/15" },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* One labeled, color-identified slice of the Overview card — Activity,
|
||||
* Violations, or Info. The color lives on the group's icon/label (a legend)
|
||||
* and, when `emphasizeNonZero` is set, on individual values that are actually
|
||||
* non-zero — never as a flat wash across numbers that don't mean anything yet.
|
||||
*/
|
||||
function StatGroup({
|
||||
icon: Icon,
|
||||
label,
|
||||
color,
|
||||
items,
|
||||
emphasizeNonZero = false,
|
||||
}: {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
color: keyof typeof STAT_GROUP_COLORS;
|
||||
items: { label: string; value: number }[];
|
||||
emphasizeNonZero?: boolean;
|
||||
}) {
|
||||
const c = STAT_GROUP_COLORS[color];
|
||||
return (
|
||||
<div>
|
||||
<dt className="text-xs text-muted-foreground">{label}</dt>
|
||||
<dd className="text-lg font-semibold text-foreground mt-0.5">{value.toLocaleString()}</dd>
|
||||
<div className={`rounded-lg ${c.bg} ring-1 ${c.ring} p-4`}>
|
||||
<div className={`flex items-center gap-1.5 text-xs font-semibold ${c.text} mb-3`}>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
</div>
|
||||
<dl className="space-y-2.5">
|
||||
{items.map((item) => {
|
||||
const flagged = emphasizeNonZero && item.value > 0;
|
||||
return (
|
||||
<div key={item.label} className="flex items-baseline justify-between gap-3">
|
||||
<dt className="text-xs text-muted-foreground">{item.label}</dt>
|
||||
<dd className={`text-sm font-semibold tabular-nums ${flagged ? c.text : "text-foreground"}`}>
|
||||
{item.value.toLocaleString()}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user