▍ humdrum codex / soft
2.6 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
"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>
  );
}