Fix routing, data fetching, and rendering issues in CRDC demo app:
Deploy to git-pages / deploy (push) Successful in 10s
Deploy to git-pages / deploy (push) Successful in 10s
- 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
This commit is contained in:
+4
-3
@@ -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() {
|
||||
<div className="cv-app">
|
||||
{/* Header */}
|
||||
<header className="cv-header cv-wrap">
|
||||
<a href="/" aria-label="Civilytics" style={{ display: 'block' }}>
|
||||
<a href="/crdc-demo" aria-label="Civilytics" style={{ display: 'block' }}>
|
||||
<img
|
||||
src="/civilytics-logo.svg"
|
||||
alt="Civilytics — social science for the public good"
|
||||
@@ -81,7 +82,7 @@ export default function App() {
|
||||
<button
|
||||
className="btn-outline"
|
||||
style={{ margin: 'var(--space-3) 0', display: 'block' }}
|
||||
onClick={() => { window.location.href = '/' }}
|
||||
onClick={() => { window.location.href = '/crdc-demo/' }}
|
||||
>
|
||||
← Search another district
|
||||
</button>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ padding: 'var(--space-5) 0 var(--space-7)' }}>
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user