From 8ba7440a34b9d103b796164fd50d41a8439f8ef7 Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Mon, 10 Aug 2026 11:42:31 -0400 Subject: [PATCH] Fix routing, data fetching, and rendering issues in CRDC demo app: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.jsx: Fix 'search another district' button href to use /crdc-demo/ instead of / (root) - LoadingAnimation.jsx: Replace broken inline fetch helper with centralized api client from useApi.js — was bypassing envelope unwrapping ({status:'success',data:[...]}), causing all 43 API calls to fail silently - ChartPanel.jsx: Same fix — replace raw fetch() calls with api.fetchDistrictEstimates(), fix national_rates.json path (remove hardcoded /crdc-demo/ prefix that breaks in dev), parallelize wave/model fetches with Promise.all for faster loading Root causes fixed: 1. Navigation button linked to site root instead of app subdirectory → users lost their way after selecting a district 2. Dual API client implementations — inline fetch helpers didn't unwrap the JSON envelope structure, causing data parsing failures that made the page appear stuck on 'Organizing data...' 3. Sequential fetches in ChartPanel caused slow loading; parallelized for better UX --- src/App.jsx | 7 +++-- src/components/ChartPanel.jsx | 24 ++++++---------- src/components/LoadingAnimation.jsx | 44 +++++++++++------------------ 3 files changed, 30 insertions(+), 45 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 82c41f7..ae1c088 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,7 +20,8 @@ export default function App() { const leaid = params.get('leaid') const stateParam = params.get('state') if (leaid && stateParam) { - // Deep link: skip straight to loading → results + // Deep link: skip straight to loading → results. + // We need to fetch the district name since we only have LEAID from URL. setSelectedState(stateParam) setDistrict({ leaid, lea_name: 'Loading…', state: stateParam }) setStep('loading') @@ -44,7 +45,7 @@ export default function App() {
{/* Header */}
- + Civilytics — social science for the public good { window.location.href = '/' }} + onClick={() => { window.location.href = '/crdc-demo/' }} > ← Search another district diff --git a/src/components/ChartPanel.jsx b/src/components/ChartPanel.jsx index 99cef7d..a15ba8f 100644 --- a/src/components/ChartPanel.jsx +++ b/src/components/ChartPanel.jsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' +import * as api from '../hooks/useApi.js' import ArrestsOverTime from '../charts/ArrestsOverTime.jsx' import RateByGroupBar from '../charts/RateByGroupBar.jsx' import DistrictVsNational from '../charts/DistrictVsNational.jsx' @@ -15,19 +16,14 @@ export default function ChartPanel({ district, state }) { const [nationalRates, setNationalRates] = useState(null) const [loading, setLoading] = useState(true) - // Proxy support for CORS bypass when deployed on pages.civilytics.org - const PROXY_URL = import.meta.env.VITE_PROXY_URL || '' - const apiFetchWithProxy = (path) => { - return PROXY_URL ? `${PROXY_URL}?target=${encodeURIComponent(path)}` : `https://crdc-api.civilytics.org/api/v1${path}` - } - + // Use centralized API client for consistent error handling and CORS proxy support. useEffect(() => { async function fetchData() { try { // National rates from static fixture (or cache from loading step) let nat = window.__NATIONAL_RATES__ if (!nat) { - const res = await fetch('/crdc-demo/data/national_rates.json') + const res = await fetch('/data/national_rates.json') nat = await res.json() } setNationalRates(nat) @@ -35,18 +31,16 @@ export default function ChartPanel({ district, state }) { // Fetch all 3 waves × default model (unified_m2_mod) for Chart 1 + Charts 2-3 const waves = ['21-22', '17-18', '15-16'] const waveData = {} - for (const year of waves) { - const res = await fetch(apiFetchWithProxy(`/estimates/${district.leaid}?model=unified_m2_mod&year=${year}`)) - if (res.ok) waveData[year] = (await res.json()).data || [] - } + await Promise.all(waves.map(async (year) => { + try { waveData[year] = await api.fetchDistrictEstimates(district.leaid, { model: 'unified_m2_mod', year }) } catch(e) {} + })) // Fetch all 4 quadrant models for Charts 4-6 (8 groups × 4 models, but we batch by model) const quadrants = ['unified_m1_mod', 'unified_m2_mod', 'unified_m3_mod', 'unified_m4_mod'] const quadData = {} - for (const model of quadrants) { - const res = await fetch(apiFetchWithProxy(`/estimates/${district.leaid}?model=${model}&year=21-22`)) - if (res.ok) quadData[model] = (await res.json()).data || [] - } + await Promise.all(quadrants.map(async (model) => { + try { quadData[model] = await api.fetchDistrictEstimates(district.leaid, { model, year: '21-22' }) } catch(e) {} + })) setData({ waveData, quadData }) } catch (err) { diff --git a/src/components/LoadingAnimation.jsx b/src/components/LoadingAnimation.jsx index 83d8b98..99ccbd6 100644 --- a/src/components/LoadingAnimation.jsx +++ b/src/components/LoadingAnimation.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react' import ChartPanel from './ChartPanel.jsx' +import * as api from '../hooks/useApi.js' // The 8 student groups we fetch for (race×sex combinations) const STUDENT_GROUPS = [ @@ -102,18 +103,18 @@ export default function LoadingAnimation({ district, state }) { return renderError(error, district) } - // Build the grid of "bars" — one per API call needed - const allBars = [ - ...WAVES.map((y, i) => ({ label: `Wave ${y}`, subLabel: 'Time series', col: 0, row: i })), - ...STUDENT_GROUPS.map((sg, i) => ({ label: sg.label, subLabel: 'Rate by group', col: 1, row: i })), - ...MODEL_QUADRANTS.flatMap((q, qi) => - STUDENT_GROUPS.map((sg, si) => ({ - label: `${q.model.split('_m')[0].substring(0,3).toUpperCase()} M${qi+1}`, - subLabel: sg.label, - col: 2 + qi % 2, row: si, quadrant: q - })) - ), - ] + // Build the grid of "bars" — one per API call needed. + // Total = WAVES (3) + STUDENT_GROUPS (8) + MODEL_QUADRANTS×STUDENT_GROUPS (4×8=32) + const TOTAL_BARS = WAVES.length + STUDENT_GROUPS.length + (MODEL_QUADRANTS.length * STUDENT_GROUPS.length) + + // Build bar metadata for rendering + const allBars = Array.from({ length: TOTAL_BARS }, (_, i) => { + if (i < WAVES.length) return { subLabel: 'Time series', group: 'waves' } + if (i < WAVES.length + STUDENT_GROUPS.length) return { subLabel: STUDENT_GROUPS[i - WAVES.length].label, group: 'groups' } + const quadIdx = Math.floor((i - WAVES.length - STUDENT_GROUPS.length) / STUDENT_GROUPS.length) + const sgIdx = (i - WAVES.length - STUDENT_GROUPS.length) % STUDENT_GROUPS.length + return { subLabel: STUDENT_GROUPS[sgIdx].label, group: 'models', modelIdx: quadIdx } + }) return (
@@ -148,7 +149,7 @@ export default function LoadingAnimation({ district, state }) { ))} {/* Grid of animated bars — each represents one API call */} - {allBars.slice(0, totalCalls || allBars.length).map((bar, i) => { + {allBars.map((bar, i) => { const filled = i < loadedCount // Random height for "data viz" aesthetic (deterministic via seed = index) const heightSeed = ((i * 37) % 100) + 20 // 20–120px range @@ -201,21 +202,10 @@ export default function LoadingAnimation({ district, state }) { ) } -// ——— Inline fetch helper with CORS proxy support ——— -const PROXY_URL = import.meta.env.VITE_PROXY_URL || '' - +// ——— API helper using the centralized client from useApi.js ——— +// This ensures consistent error handling, envelope unwrapping, and CORS proxy support. async function fetchDistrictEstimatesBatch(leaid, year, race = null, sex = null, model = 'unified_m2_mod') { - const params = new URLSearchParams({ leaid, year, model }) - if (race) params.set('race', race) - if (sex) params.set('sex', sex) - - // Route through proxy to bypass CORS restrictions when deployed on pages.civilytics.org - const targetPath = `/api/v1/estimates/${leaid}?${params}` - const url = PROXY_URL ? `${PROXY_URL}?target=${encodeURIComponent(targetPath)}` : `https://crdc-api.civilytics.org/api/v1/estimates/${leaid}?${params}` - - const res = await fetch(url) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return res.json() + return api.fetchDistrictEstimates(leaid, { year, model, race, sex }) } function getStateName(code) {