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>
);
}
|