Download src/components/TopicSelector.jsx from NKessler/Horizon: direct link, hf CLI and curl.
- Browser
- Download file 4.09 kB
-
https://huggingface.co/NKessler/Horizon/resolve/main/src/components/TopicSelector.jsx
- Command line
-
hf download hf://NKessler/Horizon/src/components/TopicSelector.jsx
-
curl -L -o TopicSelector.jsx https://huggingface.co/NKessler/Horizon/resolve/main/src/components/TopicSelector.jsx
4.09 kB
| 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> | |
| ); | |
| } | |