dodil/erp-ui

src/components/timeline/Timeline.tsx

/**
 * Timeline — what was said and done, newest first, grouped by day. The record of a relationship: calls,
 * emails, meetings, notes, and tasks with their state.
 */
import { Badge, Box, Flex, Text } from "@radix-ui/themes";
import type { ReactNode } from "react";

import { formatDate } from "../../lib/format";
import { StatusBadge } from "../status-badge/StatusBadge";

export interface TimelineItem {
  id: string;
  kind: string;
  title: string;
  body?: string;
  /** ISO time it happened (or is due). */
  at: string;
  actor?: string;
  status?: string | null;
}

export function Timeline({ items, empty }: { items: TimelineItem[]; empty?: ReactNode }) {
  if (!items.length) return <Text color="gray">{empty ?? "Nothing logged yet."}</Text>;
  const sorted = [...items].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
  const days: [string, TimelineItem[]][] = [];
  for (const it of sorted) {
    const day = it.at.slice(0, 10);
    const last = days[days.length - 1];
    if (last && last[0] === day) last[1].push(it); else days.push([day, [it]]);
  }
  return (
    <Flex direction="column" gap="4" asChild>
      <ol aria-label="Timeline" style={{ listStyle: "none", padding: 0, margin: 0 }}>
        {days.map(([day, list]) => (
          <li key={day}>
            <Text as="div" size="1" color="gray" weight="medium" mb="2">{formatDate(day)}</Text>
            <Flex direction="column" gap="3" asChild>
              <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
                {list.map((it) => (
                  <li key={it.id}>
                    <Flex gap="3" align="start">
                      <Badge variant="soft" color="gray" style={{ minWidth: 64, justifyContent: "center" }}>
                        {it.kind.toLowerCase()}
                      </Badge>
                      <Box style={{ flex: 1 }}>
                        <Flex gap="2" align="center">
                          <Text weight="medium">{it.title}</Text>
                          {it.status && <StatusBadge status={it.status} />}
                        </Flex>
                        {it.body && <Text as="p" size="2" style={{ whiteSpace: "pre-wrap" }}>{it.body}</Text>}
                        {it.actor && <Text as="p" size="1" color="gray">{it.actor}</Text>}
                      </Box>
                    </Flex>
                  </li>
                ))}
              </ul>
            </Flex>
          </li>
        ))}
      </ol>
    </Flex>
  );
}