File size: 4,089 Bytes
4083225 | 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 | import { useEffect, useRef, useState } from "react";
import { X } from "lucide-react";
import "./TopicSelector.css";
import { DEFAULT_NEWS_TIMEFRAME, NEWS_TIMEFRAMES } from "../constants/timeframes.js";
const SUGGESTIONS = [
"Russia–Ukraine war",
"Israel–Gaza conflict",
"Sahel coups",
"South China Sea disputes",
"Climate negotiations at COP",
"Migration across the Mediterranean",
];
export default function TopicSelector({ initialQuery, onSubmit, onCancel }) {
const [topic, setTopic] = useState(initialQuery?.topic ?? "");
const [newsTimeframe, setNewsTimeframe] = useState(
initialQuery?.newsTimeframe ?? DEFAULT_NEWS_TIMEFRAME
);
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
useEffect(() => {
setTopic(initialQuery?.topic ?? "");
setNewsTimeframe(initialQuery?.newsTimeframe ?? DEFAULT_NEWS_TIMEFRAME);
}, [initialQuery]);
useEffect(() => {
function onKey(e) {
if (e.key === "Escape" && onCancel) onCancel();
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onCancel]);
const canSubmit = topic.trim().length > 0;
function submit(e) {
e?.preventDefault();
if (!canSubmit) return;
onSubmit({
topic: topic.trim(),
newsTimeframe,
});
}
return (
<div className="modal" role="dialog" aria-modal="true" aria-label="Define a topic">
<div className="modal__backdrop" onClick={onCancel} />
<form className="modal__panel" onSubmit={submit}>
<header className="modal__header">
<div>
<div className="label">Define a topic</div>
<h2 className="modal__title">Open an investigation</h2>
</div>
{onCancel && (
<button
type="button"
className="ghost modal__close"
aria-label="Close"
onClick={onCancel}
>
<X size={16} strokeWidth={1.5} />
</button>
)}
</header>
<div className="modal__body">
<label className="field">
<span className="label">Topic / query</span>
<input
ref={inputRef}
type="text"
value={topic}
onChange={(e) => setTopic(e.target.value)}
placeholder="e.g. Russia–Ukraine war, Gaza ceasefire negotiations…"
/>
</label>
<label className="field">
<span className="label">News timeframe</span>
<select
value={newsTimeframe}
onChange={(e) => setNewsTimeframe(e.target.value)}
>
{NEWS_TIMEFRAMES.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</label>
<div className="suggestions">
<div className="label">Suggested investigations</div>
<div className="suggestions__chips">
{SUGGESTIONS.map((s) => (
<button
key={s}
type="button"
className="chip"
onClick={() => setTopic(s)}
>
{s}
</button>
))}
</div>
</div>
</div>
<footer className="modal__footer">
<p className="modal__hint">
FrameVis will compile coverage from international wires, national
press, and partisan outlets — then surface where their narratives
diverge.
</p>
<div className="modal__actions">
{onCancel && (
<button type="button" onClick={onCancel}>
Cancel
</button>
)}
<button type="submit" className="primary" disabled={!canSubmit}>
Open investigation
</button>
</div>
</footer>
</form>
</div>
);
}
|