import { useEffect, useMemo, useState } from "react";
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import Sidebar from "./components/Sidebar.jsx";
import TopBar from "./components/TopBar.jsx";
import MobileMenu from "./components/MobileMenu.jsx";
import TopicSelector from "./components/TopicSelector.jsx";
import MapView from "./pages/MapView.jsx";
import TimelineView from "./pages/TimelineView.jsx";
import ContrastingGroupsView from "./pages/ContrastingGroupsView.jsx";
import { fetchMeta } from "./api/client.js";
import { DEFAULT_NEWS_TIMEFRAME } from "./constants/timeframes.js";
const STORAGE_KEY = "FrameVis.query.v1";
const DEFAULT_QUERY = {
topic: "",
newsTimeframe: DEFAULT_NEWS_TIMEFRAME,
};
function normalizeStoredQuery(raw) {
try {
const parsed = JSON.parse(raw);
return {
topic: typeof parsed.topic === "string" ? parsed.topic : "",
newsTimeframe: parsed.newsTimeframe ?? DEFAULT_NEWS_TIMEFRAME,
};
} catch {
return DEFAULT_QUERY;
}
}
export default function App() {
const location = useLocation();
const [query, setQuery] = useState(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return { ...DEFAULT_QUERY, ...normalizeStoredQuery(raw) };
} catch {
/* ignore */
}
return DEFAULT_QUERY;
});
const [meta, setMeta] = useState(null);
const [metaLoading, setMetaLoading] = useState(false);
const [selectorOpen, setSelectorOpen] = useState(!query.topic);
const [isCompact, setIsCompact] = useState(() =>
typeof window !== "undefined" ? window.innerWidth <= 900 : false
);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const topicKey = query.topic?.trim() ?? "";
const newsTimeframeKey = query.newsTimeframe ?? DEFAULT_NEWS_TIMEFRAME;
useEffect(() => {
if (!topicKey) return;
let cancelled = false;
setMetaLoading(true);
const persist = {
topic: topicKey,
newsTimeframe: newsTimeframeKey,
};
fetchMeta(persist)
.then((m) => !cancelled && setMeta(m))
.finally(() => !cancelled && setMetaLoading(false));
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(persist));
} catch {
/* ignore quota errors */
}
return () => {
cancelled = true;
};
}, [topicKey, newsTimeframeKey]);
useEffect(() => {
const onResize = () => setIsCompact(window.innerWidth <= 900);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
useEffect(() => {
setMobileMenuOpen(false);
}, [location.pathname]);
const hasTopic = Boolean(query.topic);
const routedViews = useMemo(
() => (
FrameVis · Intelligence Terminal
Compare how a single conflict, policy, or event is framed across countries, sources, and time. Choose a topic to begin.