File size: 6,074 Bytes
61bbb51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// "Cite this platform" dialog, reached from the landing-page footer.
// Requested by Ali Hajian (2026-09-09), modelled on Google Scholar's Cite
// button: the same reference in five styles, click one to copy it, with
// BibTeX and RIS for reference managers underneath.
//
// Every string comes from GET /api/citation (backend/app/citation.py). The UI
// formats nothing itself, so the dialog, the repo's CITATION.cff and the
// version stamped into run metadata cannot drift apart.
import { useEffect, useRef, useState } from "react";
import { api } from "./api.js";

// Clipboard access needs a secure context. It is there on the deployment
// (HTTPS) and on localhost, but not on a plain-HTTP LAN address, which is how
// a lab machine sometimes reaches a dev instance - hence the fallback rather
// than a dead button.
async function copyText(text) {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    try {
      const area = document.createElement("textarea");
      area.value = text;
      area.setAttribute("readonly", "");
      area.style.position = "fixed";
      area.style.opacity = "0";
      document.body.appendChild(area);
      area.select();
      const ok = document.execCommand("copy");
      document.body.removeChild(area);
      return ok;
    } catch {
      return false;
    }
  }
}

export default function CiteDialog({ onClose }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState("");
  const [copied, setCopied] = useState("");  // id of the row just copied
  const [exportView, setExportView] = useState("");  // "" | "bibtex" | "ris"
  const dialogRef = useRef(null);

  useEffect(() => {
    api.citation().then(setData).catch((err) => setError(err.message));
  }, []);

  useEffect(() => {
    // Escape closes, and focus moves into the dialog so a keyboard user is
    // not left behind on the footer link.
    function onKey(e) {
      if (e.key === "Escape") onClose();
    }
    document.addEventListener("keydown", onKey);
    dialogRef.current?.focus();
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  useEffect(() => {
    if (!copied) return undefined;
    const t = setTimeout(() => setCopied(""), 1800);
    return () => clearTimeout(t);
  }, [copied]);

  async function copy(id, text, node) {
    const ok = await copyText(text);
    setCopied(ok ? id : "");
    if (ok) {
      setError("");
      return;
    }
    // No clipboard (an old browser, or a plain-HTTP address): select the
    // reference so the next keystroke copies it, rather than leaving the
    // click doing nothing.
    if (node) {
      const range = document.createRange();
      range.selectNodeContents(node);
      const sel = window.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);
    }
    setError("This browser blocked the clipboard. The citation is selected: press Ctrl+C or Cmd+C.");
  }

  return (
    <div className="modal-backdrop" onMouseDown={onClose}>
      <div
        className="modal modal-wide cite-modal"
        role="dialog"
        aria-modal="true"
        aria-label="Cite this platform"
        tabIndex={-1}
        ref={dialogRef}
        onMouseDown={(e) => e.stopPropagation()}
      >
        <div className="modal-head">
          <h3>Cite this platform</h3>
          <button className="linkish cite-close" onClick={onClose} aria-label="Close">
            ✕
          </button>
        </div>

        {error && <div className="error-banner">{error}</div>}
        {!data && !error && <p className="small muted">Loading…</p>}

        {data && (
          <>
            <p className="hint">
              Version {data.platform.version}. Click a citation to copy it.
            </p>

            <div className="cite-list">
              {data.styles.map((s) => (
                <button
                  type="button"
                  className="cite-row"
                  key={s.id}
                  onClick={(e) =>
                    copy(s.id, data.platform.styles[s.id], e.currentTarget.querySelector(".cite-text"))
                  }
                  title={`Copy the ${s.label} citation`}
                >
                  <span className="cite-style">{s.label}</span>
                  <span className="cite-text">{data.platform.styles[s.id]}</span>
                  <span className="cite-copied" aria-live="polite">
                    {copied === s.id ? "Copied" : ""}
                  </span>
                </button>
              ))}
            </div>

            <div className="cite-also">
              <b>Also cite the method.</b> Results produced here rest on the CCR
              papers, so a methods section needs them alongside the platform.
              The BibTeX and RIS below contain all three.
              <ul>
                {data.method.map((m) => (
                  <li key={m.key}>{m.apa}</li>
                ))}
              </ul>
            </div>

            <div className="cite-actions">
              <button
                className="ghost"
                onClick={() => setExportView(exportView === "bibtex" ? "" : "bibtex")}
              >
                BibTeX
              </button>
              <button
                className="ghost"
                onClick={() => setExportView(exportView === "ris" ? "" : "ris")}
              >
                RIS (EndNote, Zotero, Mendeley)
              </button>
            </div>

            {exportView && (
              <div className="cite-export">
                <pre>{data[exportView]}</pre>
                <button
                  className="ghost"
                  onClick={(e) =>
                    copy(exportView, data[exportView], e.currentTarget.parentElement.querySelector("pre"))
                  }
                >
                  {copied === exportView ? "Copied" : `Copy ${exportView === "ris" ? "RIS" : "BibTeX"}`}
                </button>
              </div>
            )}
          </>
        )}
      </div>
    </div>
  );
}