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:
parent
66816e2182
commit
7f27b5e823
2 changed files with 56 additions and 34 deletions
|
|
@ -28,14 +28,16 @@ export function QuickStats({
|
|||
totalMembers,
|
||||
}: QuickStatsProps) {
|
||||
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">
|
||||
<ItemMedia variant="icon">
|
||||
<CrosshairIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Active Campaigns</ItemTitle>
|
||||
<ItemDescription>{activeCampaigns}</ItemDescription>
|
||||
<ItemTitle className="whitespace-nowrap">Active Campaigns</ItemTitle>
|
||||
<ItemDescription className="text-2xl font-semibold text-foreground leading-none">
|
||||
{activeCampaigns}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted" size="sm">
|
||||
|
|
@ -43,8 +45,10 @@ export function QuickStats({
|
|||
<CrosshairIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Total Missions</ItemTitle>
|
||||
<ItemDescription>{totalMissions}</ItemDescription>
|
||||
<ItemTitle className="whitespace-nowrap">Total Missions</ItemTitle>
|
||||
<ItemDescription className="text-2xl font-semibold text-foreground leading-none">
|
||||
{totalMissions}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted" size="sm">
|
||||
|
|
@ -52,8 +56,10 @@ export function QuickStats({
|
|||
<Building2Icon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Structures</ItemTitle>
|
||||
<ItemDescription>{totalStructures}</ItemDescription>
|
||||
<ItemTitle className="whitespace-nowrap">Structures</ItemTitle>
|
||||
<ItemDescription className="text-2xl font-semibold text-foreground leading-none">
|
||||
{totalStructures}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted" size="sm">
|
||||
|
|
@ -61,8 +67,10 @@ export function QuickStats({
|
|||
<ShieldIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Factions</ItemTitle>
|
||||
<ItemDescription>{totalFactions}</ItemDescription>
|
||||
<ItemTitle className="whitespace-nowrap">Factions</ItemTitle>
|
||||
<ItemDescription className="text-2xl font-semibold text-foreground leading-none">
|
||||
{totalFactions}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
<Item variant="muted" size="sm">
|
||||
|
|
@ -70,8 +78,10 @@ export function QuickStats({
|
|||
<UsersIcon />
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle>Operators</ItemTitle>
|
||||
<ItemDescription>{totalMembers}</ItemDescription>
|
||||
<ItemTitle className="whitespace-nowrap">Operators</ItemTitle>
|
||||
<ItemDescription className="text-2xl font-semibold text-foreground leading-none">
|
||||
{totalMembers}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import {
|
|||
ItemHeader,
|
||||
ItemTitle,
|
||||
} from "@/components/ui/item";
|
||||
import { ScrollTextIcon } from "lucide-react";
|
||||
import { BookOpenTextIcon, ScrollTextIcon } from "lucide-react";
|
||||
import { formatType } from "@/utils/event-log/formatType";
|
||||
|
||||
interface EventEntry {
|
||||
id: number;
|
||||
|
|
@ -64,36 +65,47 @@ export function RecentEvents({ events, currentUserId }: RecentEventsProps) {
|
|||
</div>
|
||||
</ItemHeader>
|
||||
<ItemContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
{events.map((event) => {
|
||||
<div className="flex flex-col">
|
||||
{events.map((event, index) => {
|
||||
const actor =
|
||||
typeof event.actor === "object" && event.actor !== null
|
||||
? (event.actor as User)
|
||||
: null;
|
||||
const actorId =
|
||||
actor?.id ??
|
||||
(typeof event.actor === "number" ? event.actor : null);
|
||||
const isYou = actorId != null && actorId === currentUserId;
|
||||
const actorName =
|
||||
actor?.id === currentUserId
|
||||
? "You"
|
||||
: actor?.displayName ?? actor?.username ?? null;
|
||||
actor?.displayName ?? actor?.username ?? null;
|
||||
const actorLabel = isYou ? "You" : actorName;
|
||||
const isNarrative = event.system === false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
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">
|
||||
{timeAgo(event.timestamp)}
|
||||
</span>
|
||||
<div className="flex flex-col gap-0.5 min-w-0">
|
||||
<p className="text-sm leading-snug">
|
||||
{actorName && (
|
||||
<span className="font-medium">{actorName} </span>
|
||||
)}
|
||||
{event.message}
|
||||
</p>
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground font-mono">
|
||||
{event.type}
|
||||
</span>
|
||||
<div key={event.id}>
|
||||
<div
|
||||
className={`flex items-start gap-3 py-2 text-sm ${isNarrative ? "pl-3 border-l-2 border-amber-500/40" : ""}`}
|
||||
>
|
||||
{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)}
|
||||
</span>
|
||||
{actorLabel && (
|
||||
<span className={`font-medium ${isYou ? "text-foreground" : ""}`}>
|
||||
{actorLabel}
|
||||
</span>
|
||||
)}
|
||||
<span className="capitalize">{formatType(event.type)}</span>
|
||||
</div>
|
||||
<span className={`min-w-0 ${isNarrative ? "text-amber-100/90" : ""}`}>
|
||||
{event.message}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{index < events.length - 1 && <div className="border-t border-border" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Reference in a new issue