devist 0.20.0

Project bootstrap CLI for AI-assisted development. Spin up new projects from templates, manage backends, and keep your codebase comprehensible.
import { EventRow } from "@/components/EventRow";
import { useI18n } from "@/i18n/I18nProvider";
import { type Stats, fetchStats, listEvents } from "@/lib/queries";
import { useRealtimeWorkerEvents } from "@/lib/realtime";
import type { WorkerEvent } from "@/types";
import { useEffect, useState } from "react";

export default function Overview() {
  const { t } = useI18n();
  const [stats, setStats] = useState<Stats | null>(null);
  const [recent, setRecent] = useState<WorkerEvent[]>([]);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let alive = true;
    Promise.all([fetchStats(), listEvents({ limit: 25 })])
      .then(([s, r]) => {
        if (!alive) return;
        setStats(s);
        setRecent(r);
      })
      .catch((e) => alive && setError(String(e?.message ?? e)));
    return () => {
      alive = false;
    };
  }, []);

  useRealtimeWorkerEvents({
    onInsert: (ev) => {
      setRecent((prev) => [ev, ...prev].slice(0, 25));
      setStats((prev) => {
        if (!prev) return prev;
        const isToday =
          new Date(ev.created_at).toDateString() === new Date().toDateString();
        return {
          events_today: prev.events_today + (isToday ? 1 : 0),
          advice_this_week:
            prev.advice_this_week + (ev.event_type === "advice" ? 1 : 0),
          warn_block_this_week:
            prev.warn_block_this_week +
            (ev.severity === "warn" || ev.severity === "block" ? 1 : 0),
        };
      });
    },
    onUpdate: (ev) => {
      setRecent((prev) => prev.map((e) => (e.id === ev.id ? { ...e, ...ev } : e)));
    },
  });

  return (
    <div className="p-6 space-y-6">
      <header>
        <h1 className="text-xl font-semibold">{t("overview.title")}</h1>
      </header>

      {error && (
        <div className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-700">
          {error}
        </div>
      )}

      <section className="grid grid-cols-1 sm:grid-cols-3 gap-3">
        <Stat label={t("overview.stat.eventsToday")} value={stats?.events_today} />
        <Stat label={t("overview.stat.adviceWeek")} value={stats?.advice_this_week} />
        <Stat label={t("overview.stat.warnBlock")} value={stats?.warn_block_this_week} />
      </section>

      <section>
        <h2 className="text-sm font-semibold mb-2">{t("overview.recent")}</h2>
        <div className="rounded-md border bg-card">
          {recent.length === 0 && (
            <div className="p-4 text-sm text-muted-foreground">{t("overview.empty")}</div>
          )}
          {recent.map((ev) => (
            <EventRow key={ev.id} event={ev} />
          ))}
        </div>
      </section>
    </div>
  );
}

function Stat({ label, value }: { label: string; value: number | undefined }) {
  return (
    <div className="rounded-md border bg-card p-4">
      <div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
      <div className="mt-1 text-2xl font-semibold tabular-nums">{value ?? "—"}</div>
    </div>
  );
}