▍ humdrum codex / soft
2.8 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
"use client";

import Link from "next/link";
import { formatDistanceToNow } from "date-fns";
import type { SourceState } from "@/lib/types";
import { CardBody } from "./CardBodies";

export function CardShell({
  title,
  meta,
  onRefresh,
  refreshing,
  children,
}: {
  title: string;
  meta?: string;
  onRefresh?: () => void;
  refreshing?: boolean;
  children: React.ReactNode;
}) {
  return (
    <section
      className="card p-4 flex flex-col"
      style={{
        background: "var(--bg-card)",
        border: "1px solid var(--border)",
        borderRadius: 8,
        height: "100%",
        minHeight: 0,
      }}
    >
      <header className="flex items-center justify-between mb-3 shrink-0">
        <h2
          className="text-xs uppercase tracking-widest"
          style={{ fontFamily: "var(--font-display)", color: "var(--text-muted)" }}
        >
          {title}
        </h2>
        <div className="flex items-center gap-2">
          {meta && <span className="text-xs" style={{ color: "var(--text-faint)" }}>{meta}</span>}
          {onRefresh && (
            <button
              onClick={onRefresh}
              disabled={refreshing}
              aria-label={`Refresh ${title}`}
              className="text-xs px-1"
              style={{ color: "var(--text-muted)", opacity: refreshing ? 0.5 : 1 }}
            >
              {refreshing ? "…" : "↻"}
            </button>
          )}
        </div>
      </header>
      <div className="flex-1 overflow-y-auto min-h-0">{children}</div>
    </section>
  );
}

export function SourceCard({
  state,
  onRefresh,
  refreshing,
}: {
  state: SourceState;
  onRefresh: () => void;
  refreshing: boolean;
}) {
  const { source, configured, hasModule, snapshot } = state;

  const meta = snapshot?.ok
    ? `${formatDistanceToNow(new Date(snapshot.fetchedAt))} ago`
    : undefined;

  let body: React.ReactNode;
  if (!hasModule) {
    body = <Note>No fetcher for this source.</Note>;
  } else if (!configured) {
    body = (
      <Note>
        Needs configuration.{" "}
        <Link href="/settings" style={{ color: "var(--text-accent)" }}>
          Open settings 
        </Link>
      </Note>
    );
  } else if (!snapshot) {
    body = <Note>Loading</Note>;
  } else if (!snapshot.ok) {
    body = <Note tone="error">{snapshot.error ?? "Failed to load."}</Note>;
  } else {
    body = <CardBody kind={source.kind} payload={snapshot.payload} rows={source.rows} />;
  }

  return (
    <CardShell title={source.label} meta={meta} onRefresh={onRefresh} refreshing={refreshing}>
      {body}
    </CardShell>
  );
}

function Note({ children, tone }: { children: React.ReactNode; tone?: "error" }) {
  return (
    <p className="text-sm" style={{ color: tone === "error" ? "var(--red)" : "var(--text-faint)" }}>
      {children}
    </p>
  );
}