103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
import type { GameStructure, User } from "@/payload-types";
|
|
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
|
import { BookOpenTextIcon, ScrollTextIcon } from "lucide-react";
|
|
import { formatType } from "@/utils/event-log/formatType";
|
|
|
|
interface EventEntry {
|
|
id: number;
|
|
timestamp: string;
|
|
type: string;
|
|
message: string;
|
|
system: boolean;
|
|
actor?: number | User | null;
|
|
structure?: number | GameStructure | null;
|
|
}
|
|
|
|
interface RecentEventsProps {
|
|
events: EventEntry[];
|
|
currentUserId?: number;
|
|
}
|
|
|
|
function timeAgo(dateStr: string): string {
|
|
const now = new Date();
|
|
const date = new Date(dateStr);
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
if (diffMins < 1) return "just now";
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
return `${diffDays}d ago`;
|
|
}
|
|
|
|
export function RecentEvents({ events, currentUserId }: RecentEventsProps) {
|
|
if (events.length === 0) {
|
|
return (
|
|
<Item variant="outline">
|
|
<ItemHeader>
|
|
<div className="flex items-center gap-2">
|
|
<ScrollTextIcon className="size-4 text-muted-foreground" />
|
|
<ItemTitle>Recent Events</ItemTitle>
|
|
</div>
|
|
</ItemHeader>
|
|
<ItemContent>
|
|
<p className="text-sm text-muted-foreground italic">No recent events recorded.</p>
|
|
</ItemContent>
|
|
</Item>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Item variant="outline">
|
|
<ItemHeader>
|
|
<div className="flex items-center gap-2">
|
|
<ScrollTextIcon className="size-4 text-muted-foreground" />
|
|
<ItemTitle>Recent Events</ItemTitle>
|
|
</div>
|
|
</ItemHeader>
|
|
<ItemContent>
|
|
<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?.displayName ?? actor?.username ?? null;
|
|
const actorLabel = isYou ? "You" : actorName;
|
|
const isNarrative = event.system === false;
|
|
|
|
return (
|
|
<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>
|
|
);
|
|
})}
|
|
</div>
|
|
</ItemContent>
|
|
</Item>
|
|
);
|
|
}
|