| import { useEffect, useMemo, useRef } from "react"; |
|
|
| import { eventIcon, severityClass } from "../eventDecor"; |
| import type { CountrySnapshot, EntitySnapshot, GameLogEventSnapshot } from "../types"; |
|
|
| type EventFeedPanelProps = { |
| events: GameLogEventSnapshot[]; |
| countries?: CountrySnapshot[]; |
| entities?: EntitySnapshot[]; |
| }; |
|
|
| export function EventFeedPanel({ events, countries = [], entities = [] }: EventFeedPanelProps) { |
| const listRef = useRef<HTMLDivElement>(null); |
| const isPinnedToBottomRef = useRef(true); |
|
|
| |
| |
| const colorByCountry = useMemo( |
| () => new Map(countries.map((country) => [country.id, country.color])), |
| [countries], |
| ); |
| const countryByActor = useMemo( |
| () => new Map(entities.map((entity) => [entity.id, entity.country_id ?? null])), |
| [entities], |
| ); |
|
|
| const speechColor = (event: GameLogEventSnapshot): string | undefined => { |
| if (event.type !== "speech" || !event.actor_id) { |
| return undefined; |
| } |
| |
| let countryId = countryByActor.get(event.actor_id) ?? null; |
| if (!countryId) { |
| if (event.actor_id.startsWith("qwen")) countryId = "qwen"; |
| else if (event.actor_id.startsWith("npc")) countryId = "nemotron"; |
| } |
| return countryId ? colorByCountry.get(countryId) : undefined; |
| }; |
|
|
| useEffect(() => { |
| const list = listRef.current; |
| if (list && isPinnedToBottomRef.current) { |
| list.scrollTop = list.scrollHeight; |
| } |
| }, [events]); |
|
|
| return ( |
| <aside className="eventFeed" aria-label="Event feed"> |
| <div className="sidePanelHeader"> |
| <span className="sidePanelTitle">📰 Village Chronicle</span> |
| <span className="sidePanelHint">{events.length} events</span> |
| </div> |
| <div |
| className="eventList" |
| ref={listRef} |
| onScroll={() => { |
| const list = listRef.current; |
| if (list) { |
| isPinnedToBottomRef.current = |
| list.scrollHeight - list.scrollTop - list.clientHeight < 24; |
| } |
| }} |
| > |
| {events.length === 0 ? ( |
| <p className="eventEmpty">The village is quiet... for now.</p> |
| ) : ( |
| events.map((event, index) => { |
| const color = speechColor(event); |
| return ( |
| <div |
| className={`eventLine ${severityClass(event.severity)}`} |
| key={`${event.tick}-${index}-${event.type}`} |
| // Tint a speaker's line with their nation colour as a small |
| // highlight (left bar + faint wash); the text itself stays light. |
| style={ |
| color |
| ? { borderLeftColor: color, background: `${color}14` } |
| : undefined |
| } |
| > |
| <span className="eventTick">{event.tick}</span> |
| <span className="eventIcon" aria-hidden="true"> |
| {eventIcon(event.type)} |
| </span> |
| <span className="eventText">{event.summary}</span> |
| </div> |
| ); |
| }) |
| )} |
| </div> |
| </aside> |
| ); |
| } |
|
|