Skip to content

feat: Add frontend scaffolding to fetch campaigns/datasets from backend (Issue #206)#9

Open
echelonnought wants to merge 1 commit into
mainfrom
feat/206-fetch-campaigns-datasets
Open

feat: Add frontend scaffolding to fetch campaigns/datasets from backend (Issue #206)#9
echelonnought wants to merge 1 commit into
mainfrom
feat/206-fetch-campaigns-datasets

Conversation

@echelonnought
Copy link
Copy Markdown

@echelonnought echelonnought commented May 2, 2026

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

  • Exports API_BASE_URL (from VITE_API_BASE_URL env var, defaults to http://localhost:5000)
  • Exports API_V1_URL for the /api/v1 prefix

src/services/api.ts -- API Service Layer

Typed fetch functions for all expected endpoints:

  • fetchCampaigns(page, pageSize) -- GET /api/v1/campaigns
  • fetchCampaignById(id) -- GET /api/v1/campaigns/:id
  • fetchDatasets(page, pageSize) -- GET /api/v1/datasets
  • fetchDatasetById(id) -- GET /api/v1/datasets/:id
  • fetchDatasetsByCampaign(campaignId) -- GET /api/v1/campaigns/:campaignId/datasets
  • Generic apiFetch wrapper with structured ApiError handling

src/types/api.ts -- TypeScript Interfaces

  • Campaign: conservation project entity (name, status, partner, acreage, geometry, etc.)
  • Dataset: geospatial data layer linked to a campaign (GeoJSON, type, visibility)
  • CampaignGeometry / LatLng: geo coordinate types
  • PaginatedResponse: standard paginated API wrapper
  • ApiError: structured error type

src/hooks/useCampaigns.ts -- React Hooks

  • useCampaigns(page, pageSize): fetches campaigns with loading/error states and refetch
  • useCampaignDatasets(campaignId): fetches datasets for a campaign with cleanup

.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

Method Path Status
GET /api/v1/campaigns Not yet implemented
GET /api/v1/campaigns/:id Not yet implemented
GET /api/v1/campaigns/:id/datasets Not yet implemented
GET /api/v1/datasets Not yet implemented
GET /api/v1/datasets/:id Not yet implemented

Verification

  • TypeScript compiles cleanly (tsc --noEmit exit 0)
  • No runtime errors when backend is unavailable
  • Hooks handle errors gracefully with cleanup

Summary by CodeRabbit

  • New Features
    • Integrated backend API connectivity to fetch campaigns and datasets with pagination support
    • Backend server URL is now configurable via environment settings (defaults to http://localhost:5000)
    • Added error handling for failed backend requests with user-facing messages

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).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Backend API Integration

Layer / File(s) Summary
Environment & Configuration
.env.example, src/services/config.ts
Environment variable VITE_API_BASE_URL (default http://localhost:5000) is read and exported as API_BASE_URL and API_V1_URL (${API_BASE_URL}/api/v1).
Type Definitions
src/types/api.ts
Introduces LatLng, CampaignGeometry, Campaign, Dataset entity types, plus generic PaginatedResponse<T> and ApiError wrappers for API contracts.
API Service Layer
src/services/api.ts
Generic apiFetch<T> wrapper builds requests from API_V1_URL, standardizes error handling with typed ApiError, and exports five async functions: fetchCampaigns, fetchCampaignById, fetchDatasets, fetchDatasetById, fetchDatasetsByCampaign.
React Hooks
src/hooks/useCampaigns.ts
useCampaigns(page, pageSize) manages campaigns, loading, and error state with pagination, initial fetch on mount, and refetch capability. useCampaignDatasets(campaignId) manages dataset state, skips fetch when campaignId is null, and uses cleanup flag to prevent state updates after unmount.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops through API calls with glee,
Types and configs, oh what a spree!
Hooks fetch the campaigns so fine,
Backend and frontend—now they align!
Paginated datasets, datasets galore,
This rabbit's excited for what's in store! 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding frontend scaffolding to fetch campaigns and datasets from the backend API, with a reference to the related issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/206-fetch-campaigns-datasets

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.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@echelonnought
Copy link
Copy Markdown
Author

Detailed Implementation Notes

Architecture Decisions

1. Service layer pattern
Created a dedicated src/services/ directory with config.ts (env-based URL) and api.ts (typed fetch wrappers). This separates API concerns from UI components and makes it trivial to swap to a different data source (e.g. mock server, staging environment).

2. Generic apiFetch wrapper
All fetch calls go through a single typed wrapper that handles JSON parsing, HTTP error status codes, and structured ApiError objects. This ensures consistent error handling across all endpoints.

3. Graceful degradation in hooks
The useCampaigns hook detects when the backend is unreachable (network error / fetch failure) and sets a specific 'Backend not available' message instead of a cryptic error. This lets the UI show a helpful banner while still functioning with local fixture data.

4. Request cleanup in useCampaignDatasets
Uses a cancelled flag in the useEffect cleanup to prevent state updates after unmount. This avoids the React 'set state on unmounted component' warning when users navigate quickly.

5. Environment variable via Vite
Used VITE_API_BASE_URL (not REACT_APP_) because Vite only exposes vars prefixed with VITE_. Defaults to http://localhost:5000 matching the backend .env PORT=5000.

Integration guide

Once the backend endpoints are ready, consuming this in a component is as simple as:

import { useCampaigns } from '@/hooks/useCampaigns';
const { campaigns, loading, error } = useCampaigns();

Files Changed (5 new files, +328 lines)

  • .env.example (new)
  • src/services/config.ts (new)
  • src/services/api.ts (new)
  • src/types/api.ts (new)
  • src/hooks/useCampaigns.ts (new)

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c4ded60 and a9235bc.

📒 Files selected for processing (5)
  • .env.example
  • src/hooks/useCampaigns.ts
  • src/services/api.ts
  • src/services/config.ts
  • src/types/api.ts

Comment thread src/hooks/useCampaigns.ts
Comment on lines +36 to +59
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]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread src/hooks/useCampaigns.ts
Comment on lines +89 to +124
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]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested 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]);
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.

Comment thread src/services/api.ts
Comment on lines +25 to +30
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/services/api.ts | head -50

Repository: OpenSourceFellows/map_dashboard_hackathon

Length of output: 1888


🏁 Script executed:

cat -n src/services/api.ts | tail -100

Repository: OpenSourceFellows/map_dashboard_hackathon

Length of output: 3453


🏁 Script executed:

wc -l src/services/api.ts

Repository: 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant