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
|
"use client";
import { useState } from "react";
import { formatDistanceToNow } from "date-fns";
import type { SourceState } from "@/lib/types";
import type { ThingsPayload } from "@/lib/schemas/sources/things";
import { CardShell } from "./SourceCard";
// Things "Today" card. Reads the source snapshot; checking a box completes the
// to-do in Things 3 (via /api/integrations/things/complete) and refreshes.
export function ThingsCard({
state,
onRefresh,
refreshing,
}: {
state: SourceState;
onRefresh: () => void;
refreshing: boolean;
}) {
const [completing, setCompleting] = useState<Set<string>>(new Set());
const snapshot = state.snapshot;
const meta = snapshot?.ok ? `${formatDistanceToNow(new Date(snapshot.fetchedAt))} ago` : undefined;
async function complete(id: string) {
setCompleting((prev) => new Set(prev).add(id));
await fetch("/api/integrations/things/complete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
onRefresh();
}
let body: React.ReactNode;
if (!snapshot) {
body = <Note>Loading…</Note>;
} else if (!snapshot.ok) {
body = (
<Note tone="error">
{snapshot.error ?? "Couldn't reach Things."}
<br />
<span style={{ color: "var(--text-faint)" }}>Needs Things 3 + Automation permission.</span>
</Note>
);
} else {
const tasks = (snapshot.payload as ThingsPayload).tasks;
body =
tasks.length === 0 ? (
<Note>Nothing in Today. 🎉</Note>
) : (
<ul className="space-y-1">
{tasks.map((t) => (
<li key={t.id} className="flex items-start gap-2 text-sm">
<input
type="checkbox"
className="mt-0.5"
disabled={completing.has(t.id)}
onChange={() => complete(t.id)}
aria-label={`Complete ${t.title}`}
/>
<span style={{ opacity: completing.has(t.id) ? 0.4 : 1 }}>
{t.title}
{t.project && (
<span className="ml-1 text-xs" style={{ color: "var(--text-faint)" }}>
· {t.project}
</span>
)}
</span>
</li>
))}
</ul>
);
}
return (
<CardShell title={state.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>
);
}
|