feat: Add frontend scaffolding to fetch campaigns/datasets from backend (Issue #206)#9
feat: Add frontend scaffolding to fetch campaigns/datasets from backend (Issue #206)#9echelonnought wants to merge 1 commit into
Conversation
Implements Issue #206 — Fetch campaigns/datasets from backend. Since the backend endpoints are not yet implemented, this PR creates the complete frontend service layer ready to connect once they are. New files: - src/services/config.ts: API base URL config with VITE_API_BASE_URL env var support - src/services/api.ts: typed fetch functions for campaigns and datasets (fetchCampaigns, fetchCampaignById, fetchDatasets, fetchDatasetById, fetchDatasetsByCampaign) with generic error handling - src/types/api.ts: TypeScript interfaces for Campaign, Dataset, PaginatedResponse, ApiError, CampaignGeometry, LatLng - src/hooks/useCampaigns.ts: React hooks (useCampaigns, useCampaignDatasets) with graceful degradation when backend is unavailable - .env.example: documents the VITE_API_BASE_URL environment variable Expects backend endpoints at: - GET /api/v1/campaigns - GET /api/v1/campaigns/:id - GET /api/v1/campaigns/:id/datasets - GET /api/v1/datasets - GET /api/v1/datasets/:id TypeScript compiles cleanly (tsc --noEmit exit 0).
📝 WalkthroughWalkthroughThis PR establishes a backend API integration layer by introducing environment configuration, typed API service functions, and React hooks for managing campaign and dataset state. Five new files provide centralized API connectivity, data shape definitions, and state management for fetching paginated campaigns and associated datasets. ChangesBackend API Integration
Sequence DiagramsequenceDiagram
participant Component as React Component
participant Hook as useCampaigns / useCampaignDatasets
participant API as API Service (apiFetch)
participant Backend as Backend Server
Component->>Hook: Mount with page / campaignId
activate Hook
Note over Hook: Initialize state<br/>(campaigns/datasets, loading, error)
Hook->>Hook: useEffect triggers loadCampaigns / fetch
activate Hook
Hook->>Hook: Set loading = true
deactivate Hook
Hook->>API: fetchCampaigns(page, pageSize)<br/>or fetchDatasetsByCampaign(id)
activate API
API->>API: Construct URL from API_V1_URL
API->>Backend: fetch(URL, headers: JSON)
activate Backend
Backend-->>API: response (200 or error)
deactivate Backend
alt Success
API->>API: response.json() → T
API-->>Hook: PaginatedResponse<Campaign><br/>or Dataset[]
Hook->>Hook: setState(data)<br/>Set loading = false
else Error
API->>API: Create ApiError<br/>(status, message, details)
API-->>Hook: throw ApiError
Hook->>Hook: setState(error)<br/>Clear data<br/>Set loading = false
end
deactivate API
Hook-->>Component: Return {data, loading, error}
deactivate Hook
Component->>Component: Re-render with updated state
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
Detailed Implementation NotesArchitecture Decisions1. Service layer pattern 2. Generic apiFetch wrapper 3. Graceful degradation in hooks 4. Request cleanup in useCampaignDatasets 5. Environment variable via Vite Integration guideOnce the backend endpoints are ready, consuming this in a component is as simple as: import { useCampaigns } from '@/hooks/useCampaigns'; Files Changed (5 new files, +328 lines)
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/hooks/useCampaigns.ts`:
- Around line 89-124: When campaignId is null the effect currently clears
datasets but leaves loading and error state set, so update the early-return
branch in the useEffect to also reset loading and error: call setDatasets([]),
setLoading(false), and setError(null) when campaignId === null. Also ensure the
cancellation logic around loadDatasets (the cancelled flag and finally block
that calls setLoading(false)) remains intact so any in-flight fetch still clears
loading and avoids setting state after unmount or selection change; reference
useEffect, campaignId, loadDatasets, cancelled, setDatasets, setLoading, and
setError.
- Around line 36-59: The loadCampaigns function can suffer from race conditions
where an older fetch resolves after a newer one or after unmount; add a
request-version guard using a ref (e.g., requestIdRef) that you increment before
each fetch and capture into a local currentId; after awaiting
fetchCampaigns(page, pageSize) and in the catch block only call setCampaigns,
setError, and setLoading if requestIdRef.current === currentId; also
increment/invalidate the ref in a cleanup effect to prevent state updates after
unmount; reference loadCampaigns, fetchCampaigns, page, pageSize, setCampaigns,
setError, setLoading and the useEffect that calls loadCampaigns when applying
this change.
In `@src/services/api.ts`:
- Around line 25-30: Remove the unnecessary 'Content-Type' header from the GET
fetch wrapper so it doesn't trigger CORS preflights: in the code that calls
fetch(url, { headers: { 'Content-Type': 'application/json', 'Accept':
'application/json' } }) remove the 'Content-Type' entry and keep only 'Accept'.
If you have a shared request helper, update the logic around the fetch call (the
code creating response via fetch(...) and the headers object) to add
'Content-Type: application/json' only when a request body is present (e.g.,
POST/PUT paths) rather than for GETs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0f260522-f659-4a12-af22-d63f2c1c1f66
📒 Files selected for processing (5)
.env.examplesrc/hooks/useCampaigns.tssrc/services/api.tssrc/services/config.tssrc/types/api.ts
| const loadCampaigns = useCallback(async () => { | ||
| setLoading(true); | ||
| setError(null); | ||
|
|
||
| try { | ||
| const response = await fetchCampaigns(page, pageSize); | ||
| setCampaigns(response.data); | ||
| } catch (err) { | ||
| const apiError = err as ApiError; | ||
| const message = | ||
| apiError.status === 0 || apiError.message?.includes('fetch') | ||
| ? 'Backend not available — using local data. Start the dashboard_server to enable live data.' | ||
| : `Failed to fetch campaigns: ${apiError.message}`; | ||
| setError(message); | ||
| setCampaigns([]); | ||
| console.warn('[useCampaigns]', message); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }, [page, pageSize]); | ||
|
|
||
| useEffect(() => { | ||
| loadCampaigns(); | ||
| }, [loadCampaigns]); |
There was a problem hiding this comment.
Guard against stale campaign responses.
Line 36-59 has no cancellation/version guard, so an older page/pageSize request can resolve after a newer one and overwrite the latest state. That makes pagination/refetch nondeterministic and can also update state after unmount.
Suggested fix
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useRef } from 'react';
import { fetchCampaigns, fetchDatasetsByCampaign } from '@/services/api';
import type { Campaign, Dataset, ApiError } from '@/types/api';
@@
export const useCampaigns = (page = 1, pageSize = 50): CampaignsState => {
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
+ const requestIdRef = useRef(0);
const loadCampaigns = useCallback(async () => {
+ const requestId = ++requestIdRef.current;
setLoading(true);
setError(null);
try {
const response = await fetchCampaigns(page, pageSize);
- setCampaigns(response.data);
+ if (requestId === requestIdRef.current) {
+ setCampaigns(response.data);
+ }
} catch (err) {
const apiError = err as ApiError;
const message =
apiError.status === 0 || apiError.message?.includes('fetch')
? 'Backend not available — using local data. Start the dashboard_server to enable live data.'
: `Failed to fetch campaigns: ${apiError.message}`;
- setError(message);
- setCampaigns([]);
- console.warn('[useCampaigns]', message);
+ if (requestId === requestIdRef.current) {
+ setError(message);
+ setCampaigns([]);
+ console.warn('[useCampaigns]', message);
+ }
} finally {
- setLoading(false);
+ if (requestId === requestIdRef.current) {
+ setLoading(false);
+ }
}
}, [page, pageSize]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useCampaigns.ts` around lines 36 - 59, The loadCampaigns function
can suffer from race conditions where an older fetch resolves after a newer one
or after unmount; add a request-version guard using a ref (e.g., requestIdRef)
that you increment before each fetch and capture into a local currentId; after
awaiting fetchCampaigns(page, pageSize) and in the catch block only call
setCampaigns, setError, and setLoading if requestIdRef.current === currentId;
also increment/invalidate the ref in a cleanup effect to prevent state updates
after unmount; reference loadCampaigns, fetchCampaigns, page, pageSize,
setCampaigns, setError, setLoading and the useEffect that calls loadCampaigns
when applying this change.
| useEffect(() => { | ||
| if (campaignId === null) { | ||
| setDatasets([]); | ||
| return; | ||
| } | ||
|
|
||
| let cancelled = false; | ||
|
|
||
| const loadDatasets = async () => { | ||
| setLoading(true); | ||
| setError(null); | ||
|
|
||
| try { | ||
| const data = await fetchDatasetsByCampaign(campaignId); | ||
| if (!cancelled) { | ||
| setDatasets(data); | ||
| } | ||
| } catch (err) { | ||
| if (!cancelled) { | ||
| const apiError = err as ApiError; | ||
| setError(`Failed to fetch datasets: ${apiError.message}`); | ||
| setDatasets([]); | ||
| } | ||
| } finally { | ||
| if (!cancelled) { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| loadDatasets(); | ||
|
|
||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [campaignId]); |
There was a problem hiding this comment.
Clear loading/error state when no campaign is selected.
When campaignId becomes null, the effect clears datasets but leaves loading and error untouched. If selection is cleared mid-fetch, the spinner and stale error can stick around even though there is no active request.
Suggested fix
if (campaignId === null) {
setDatasets([]);
+ setError(null);
+ setLoading(false);
return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (campaignId === null) { | |
| setDatasets([]); | |
| return; | |
| } | |
| let cancelled = false; | |
| const loadDatasets = async () => { | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| const data = await fetchDatasetsByCampaign(campaignId); | |
| if (!cancelled) { | |
| setDatasets(data); | |
| } | |
| } catch (err) { | |
| if (!cancelled) { | |
| const apiError = err as ApiError; | |
| setError(`Failed to fetch datasets: ${apiError.message}`); | |
| setDatasets([]); | |
| } | |
| } finally { | |
| if (!cancelled) { | |
| setLoading(false); | |
| } | |
| } | |
| }; | |
| loadDatasets(); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [campaignId]); | |
| useEffect(() => { | |
| if (campaignId === null) { | |
| setDatasets([]); | |
| setError(null); | |
| setLoading(false); | |
| return; | |
| } | |
| let cancelled = false; | |
| const loadDatasets = async () => { | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| const data = await fetchDatasetsByCampaign(campaignId); | |
| if (!cancelled) { | |
| setDatasets(data); | |
| } | |
| } catch (err) { | |
| if (!cancelled) { | |
| const apiError = err as ApiError; | |
| setError(`Failed to fetch datasets: ${apiError.message}`); | |
| setDatasets([]); | |
| } | |
| } finally { | |
| if (!cancelled) { | |
| setLoading(false); | |
| } | |
| } | |
| }; | |
| loadDatasets(); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [campaignId]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useCampaigns.ts` around lines 89 - 124, When campaignId is null the
effect currently clears datasets but leaves loading and error state set, so
update the early-return branch in the useEffect to also reset loading and error:
call setDatasets([]), setLoading(false), and setError(null) when campaignId ===
null. Also ensure the cancellation logic around loadDatasets (the cancelled flag
and finally block that calls setLoading(false)) remains intact so any in-flight
fetch still clears loading and avoids setting state after unmount or selection
change; reference useEffect, campaignId, loadDatasets, cancelled, setDatasets,
setLoading, and setError.
| const response = await fetch(url, { | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Accept': 'application/json', | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/services/api.ts | head -50Repository: OpenSourceFellows/map_dashboard_hackathon
Length of output: 1888
🏁 Script executed:
cat -n src/services/api.ts | tail -100Repository: OpenSourceFellows/map_dashboard_hackathon
Length of output: 3453
🏁 Script executed:
wc -l src/services/api.tsRepository: OpenSourceFellows/map_dashboard_hackathon
Length of output: 109
Remove Content-Type header from the GET wrapper—it triggers unnecessary CORS preflights.
GET requests have no body, so Content-Type: application/json is unnecessary. The application/json value for Content-Type is not a CORS-simple header and will cause preflights on cross-origin requests. Keep Accept for all reads; add Content-Type only when the request includes a body.
Suggested fix
const response = await fetch(url, {
headers: {
- 'Content-Type': 'application/json',
'Accept': 'application/json',
},
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const response = await fetch(url, { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Accept': 'application/json', | |
| }, | |
| }); | |
| const response = await fetch(url, { | |
| headers: { | |
| 'Accept': 'application/json', | |
| }, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/api.ts` around lines 25 - 30, Remove the unnecessary
'Content-Type' header from the GET fetch wrapper so it doesn't trigger CORS
preflights: in the code that calls fetch(url, { headers: { 'Content-Type':
'application/json', 'Accept': 'application/json' } }) remove the 'Content-Type'
entry and keep only 'Accept'. If you have a shared request helper, update the
logic around the fetch call (the code creating response via fetch(...) and the
headers object) to add 'Content-Type: application/json' only when a request body
is present (e.g., POST/PUT paths) rather than for GETs.
Summary
Implements the frontend side of Issue #206 -- Fetch campaigns/datasets from backend.
Since the backend v1 API endpoints are not yet implemented, this PR creates the complete frontend service layer, types, and React hooks so everything is ready to connect once the backend ships.
New Files
src/services/config.ts -- API Configuration
src/services/api.ts -- API Service Layer
Typed fetch functions for all expected endpoints:
src/types/api.ts -- TypeScript Interfaces
src/hooks/useCampaigns.ts -- React Hooks
.env.example -- Environment Variable Documentation
Graceful Degradation
When the backend is unavailable, the hooks catch the error and set a user-friendly message: 'Backend not available -- using local data'. The app continues working with fixture data.
Backend Endpoints Required
Verification
Summary by CodeRabbit
http://localhost:5000)