1
0
Fork 0

style(dashboard): polish quick stats and recent events feed

- Emphasize stat values and keep labels on one line
- Rework the recent events feed into a two-line layout with human-readable type labels and narrative highlighting
This commit is contained in:
Jason Fraley 2026-08-11 23:25:44 -04:00
parent 66816e2182
commit 7f27b5e823
2 changed files with 56 additions and 34 deletions

View file

@ -28,14 +28,16 @@ export function QuickStats({
totalMembers, totalMembers,
}: QuickStatsProps) { }: QuickStatsProps) {
return ( return (
<div className="grid grid-cols-2 md:grid-cols-5 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 2xl:grid-cols-5 gap-3">
<Item variant="muted" size="sm"> <Item variant="muted" size="sm">
<ItemMedia variant="icon"> <ItemMedia variant="icon">
<CrosshairIcon /> <CrosshairIcon />
</ItemMedia> </ItemMedia>
<ItemContent> <ItemContent>
<ItemTitle>Active Campaigns</ItemTitle> <ItemTitle className="whitespace-nowrap">Active Campaigns</ItemTitle>
<ItemDescription>{activeCampaigns}</ItemDescription> <ItemDescription className="text-2xl font-semibold text-foreground leading-none">
{activeCampaigns}
</ItemDescription>
</ItemContent> </ItemContent>
</Item> </Item>
<Item variant="muted" size="sm"> <Item variant="muted" size="sm">
@ -43,8 +45,10 @@ export function QuickStats({
<CrosshairIcon /> <CrosshairIcon />
</ItemMedia> </ItemMedia>
<ItemContent> <ItemContent>
<ItemTitle>Total Missions</ItemTitle> <ItemTitle className="whitespace-nowrap">Total Missions</ItemTitle>
<ItemDescription>{totalMissions}</ItemDescription> <ItemDescription className="text-2xl font-semibold text-foreground leading-none">
{totalMissions}
</ItemDescription>
</ItemContent> </ItemContent>
</Item> </Item>
<Item variant="muted" size="sm"> <Item variant="muted" size="sm">
@ -52,8 +56,10 @@ export function QuickStats({
<Building2Icon /> <Building2Icon />
</ItemMedia> </ItemMedia>
<ItemContent> <ItemContent>
<ItemTitle>Structures</ItemTitle> <ItemTitle className="whitespace-nowrap">Structures</ItemTitle>
<ItemDescription>{totalStructures}</ItemDescription> <ItemDescription className="text-2xl font-semibold text-foreground leading-none">
{totalStructures}
</ItemDescription>
</ItemContent> </ItemContent>
</Item> </Item>
<Item variant="muted" size="sm"> <Item variant="muted" size="sm">
@ -61,8 +67,10 @@ export function QuickStats({
<ShieldIcon /> <ShieldIcon />
</ItemMedia> </ItemMedia>
<ItemContent> <ItemContent>
<ItemTitle>Factions</ItemTitle> <ItemTitle className="whitespace-nowrap">Factions</ItemTitle>
<ItemDescription>{totalFactions}</ItemDescription> <ItemDescription className="text-2xl font-semibold text-foreground leading-none">
{totalFactions}
</ItemDescription>
</ItemContent> </ItemContent>
</Item> </Item>
<Item variant="muted" size="sm"> <Item variant="muted" size="sm">
@ -70,8 +78,10 @@ export function QuickStats({
<UsersIcon /> <UsersIcon />
</ItemMedia> </ItemMedia>
<ItemContent> <ItemContent>
<ItemTitle>Operators</ItemTitle> <ItemTitle className="whitespace-nowrap">Operators</ItemTitle>
<ItemDescription>{totalMembers}</ItemDescription> <ItemDescription className="text-2xl font-semibold text-foreground leading-none">
{totalMembers}
</ItemDescription>
</ItemContent> </ItemContent>
</Item> </Item>
</div> </div>

View file

@ -6,7 +6,8 @@ import {
ItemHeader, ItemHeader,
ItemTitle, ItemTitle,
} from "@/components/ui/item"; } from "@/components/ui/item";
import { ScrollTextIcon } from "lucide-react"; import { BookOpenTextIcon, ScrollTextIcon } from "lucide-react";
import { formatType } from "@/utils/event-log/formatType";
interface EventEntry { interface EventEntry {
id: number; id: number;
@ -64,37 +65,48 @@ export function RecentEvents({ events, currentUserId }: RecentEventsProps) {
</div> </div>
</ItemHeader> </ItemHeader>
<ItemContent> <ItemContent>
<div className="flex flex-col gap-2"> <div className="flex flex-col">
{events.map((event) => { {events.map((event, index) => {
const actor = const actor =
typeof event.actor === "object" && event.actor !== null typeof event.actor === "object" && event.actor !== null
? (event.actor as User) ? (event.actor as User)
: null; : null;
const actorId =
actor?.id ??
(typeof event.actor === "number" ? event.actor : null);
const isYou = actorId != null && actorId === currentUserId;
const actorName = const actorName =
actor?.id === currentUserId actor?.displayName ?? actor?.username ?? null;
? "You" const actorLabel = isYou ? "You" : actorName;
: actor?.displayName ?? actor?.username ?? null; const isNarrative = event.system === false;
return ( return (
<div key={event.id}>
<div <div
key={event.id} className={`flex items-start gap-3 py-2 text-sm ${isNarrative ? "pl-3 border-l-2 border-amber-500/40" : ""}`}
className="flex items-start gap-3 text-sm py-1.5 border-b border-border/50 last:border-0"
> >
<span className="text-xs text-muted-foreground font-mono whitespace-nowrap pt-0.5"> {isNarrative && (
<BookOpenTextIcon className="size-4 text-amber-400 shrink-0 mt-0.5" />
)}
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="tabular-nums shrink-0">
{timeAgo(event.timestamp)} {timeAgo(event.timestamp)}
</span> </span>
<div className="flex flex-col gap-0.5 min-w-0"> {actorLabel && (
<p className="text-sm leading-snug"> <span className={`font-medium ${isYou ? "text-foreground" : ""}`}>
{actorName && ( {actorLabel}
<span className="font-medium">{actorName} </span> </span>
)} )}
<span className="capitalize">{formatType(event.type)}</span>
</div>
<span className={`min-w-0 ${isNarrative ? "text-amber-100/90" : ""}`}>
{event.message} {event.message}
</p>
<span className="text-[10px] uppercase tracking-wider text-muted-foreground font-mono">
{event.type}
</span> </span>
</div> </div>
</div> </div>
{index < events.length - 1 && <div className="border-t border-border" />}
</div>
); );
})} })}
</div> </div>