| 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> |
| ); |
| } |
|
|