File size: 2,897 Bytes
34b0226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useEffect, useMemo, useState } from "react";
import { docsApi } from "../api/endpoints";
import type { Doc } from "../api/types";
import { formatDate, Loading } from "./ui";

/** Searchable checklist of posts. `single` turns it into a radio list. */
export function DocPicker({
  selected,
  onChange,
  single = false,
  exclude = [],
  max,
}: {
  selected: string[];
  onChange: (ids: string[]) => void;
  single?: boolean;
  exclude?: string[];
  max?: number;
}) {
  const [docs, setDocs] = useState<Doc[] | null>(null);
  const [q, setQ] = useState("");

  useEffect(() => {
    docsApi.list({ limit: 5000 }).then((p) => setDocs(p.items));
  }, []);

  const visible = useMemo(() => {
    const skip = new Set(exclude);
    const needle = q.trim().toLowerCase();
    return (docs ?? []).filter(
      (d) => !skip.has(d.id) && (!needle || d.title.toLowerCase().includes(needle) || (d.source_title ?? "").toLowerCase().includes(needle)),
    );
  }, [docs, q, exclude]);

  if (!docs) return <Loading label="Loading posts" />;
  if (docs.length === 0) return <p className="muted">No posts yet. Connect a source first.</p>;

  const toggle = (id: string) => {
    if (single) return onChange([id]);
    if (selected.includes(id)) return onChange(selected.filter((x) => x !== id));
    if (max && selected.length >= max) return;
    onChange([...selected, id]);
  };

  return (
    <div className="picker">
      <div className="picker-bar">
        <input className="input input-sm" placeholder="Search posts" value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search posts" />
        {!single && (
          <div className="picker-actions mono muted">
            <span>{selected.length} selected{max ? ` of ${max}` : ""}</span>
            {!max && (
              <button type="button" className="link-btn" onClick={() => onChange(Array.from(new Set([...selected, ...visible.map((d) => d.id)])))}>
                Select all
              </button>
            )}
            {selected.length > 0 && (
              <button type="button" className="link-btn" onClick={() => onChange([])}>
                Clear
              </button>
            )}
          </div>
        )}
      </div>
      <ul className="picker-list">
        {visible.map((d) => (
          <li key={d.id}>
            <label className={`picker-row${selected.includes(d.id) ? " on" : ""}`}>
              <input type={single ? "radio" : "checkbox"} name="doc-picker" checked={selected.includes(d.id)} onChange={() => toggle(d.id)} />
              <span className="picker-title">{d.title}</span>
              <span className="mono muted picker-meta">
                {formatDate(d.published_at)} · {d.words} words
              </span>
            </label>
          </li>
        ))}
        {visible.length === 0 && <li className="muted">No posts match.</li>}
      </ul>
    </div>
  );
}