Frontend
The CivicPulse frontend is a React 19 single-page application built with Vite 7. It follows a modular component architecture organised by feature domain, a CSS custom properties design system, and connects to the backend over both REST and WebSocket.
Modular Component Architectureβ
The application was initially authored as a single-file component (civic-pulse.jsx, ~5,450 lines). As of v0.7, it has been decomposed into 46 modular component files organised by feature domain, while preserving all existing behaviour.
Design Principlesβ
- Domain organisation β components are grouped by feature (issues, browse, social, rankings, events, etc.), not by type (pages, modals, cards)
- Shallow import graph β each component imports only from
shared/, its sibling domain, or../../data/and../../utils/β no deep cross-domain dependencies - No new state management β global state remains in the root
Appcomponent and flows via props/callbacks (Context/Redux is a separate future effort) - CSS unchanged β all classes from
civic-pulse.cssremain globally available; no CSS modules or scoping added - Side-effect import β
FlagsCtx.jsmust be imported before any component that reads.Iconon CATEGORIES/EVENT_TYPES/NOMINATIONS, as it mutates these JSON imports at load time
Directory Structureβ
src/
βββ civic-pulse.jsx App root β state, handlers, routing (~310 lines)
βββ data/ JSON content (categories, cities, flagsβ¦)
βββ styles/civic-pulse.css Design tokens + all component styles
βββ hooks/
β βββ useFlags.js Feature flag management
β βββ useUserLocation.js Geolocation hook
β βββ usePushNotifications.js Web Push API hook
β βββ useDonors.js Donor data fetching
βββ utils/helpers.js uid, fmtDate, haversine, scoring utils
βββ components/
βββ shared/ (6) FlagsCtx, Ic, ImageMetaPanel, FlagModal, SignInModal, ShareModal
βββ media/ (4) Lightbox, UploadZone, MediaGallery, MediaStrip
βββ maps/ (2) IssueDetailMap, IssueClusterMap
βββ issues/ (9) ICard, Detail, ResolveModal, ReopenRequestInline, NewModal,
β ChatPanel, AssignmentPanel, WardSubmitModal, AccountabilityMini
βββ browse/ (4) CategoryBrowser, IssuesList, BrowseIssues, Cities
βββ social/ (5) ConvertModal, XFeed, WAFeed, IGFeed, SocialWall
βββ rankings/ (5) ContributorCard, ContributorDetail, ContributorLeaderboard,
β Leaderboard, CityRankings
βββ events/ (4) EventCard, CreateEventModal, EventDetail, Events
βββ dashboard/ (1) Dashboard
βββ support/ (4) FeatureRequestModal, JoinTeamModal, SupporterForm, SupportPage
βββ admin/ (2) AdminPanel, AccountabilityAdmin
βββ auth/ (1) UserProfile
βββ landing/ (1) Landing
The root App component in civic-pulse.jsx manages ~35 useState/useCallback declarations, handler functions, useEffect hooks, navigation logic, and wraps everything in a FlagsCtx.Provider.
Design Systemβ
CSS Custom Propertiesβ
Visual values are declared as CSS custom properties on :root. Component-level styles reference these tokens via var(), never hard-coded colours or sizes.
:root {
/* Backgrounds */
--color-bg-primary: #0f172a;
--color-bg-secondary: #1a2332;
--color-bg-surface: #1e293b;
--color-bg-elevated: #243044;
/* Accent */
--color-accent: #6366f1;
--color-accent-hover: #818cf8;
--color-accent-muted: rgba(99, 102, 241, 0.15);
/* Text */
--color-text-primary: #f1f5f9;
--color-text-secondary:#94a3b8;
--color-text-muted: #64748b;
/* Status */
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #3b82f6;
/* Spacing */
--space-1: 4px; --space-2: 8px; --space-3: 12px;
--space-4: 16px; --space-6: 24px; --space-8: 32px;
/* Radii */
--radius-sm: 4px; --radius-md: 8px;
--radius-lg: 12px; --radius-xl: 16px;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4);
--shadow-md: 0 4px 12px rgba(0,0,0,0.5);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.6);
}
Light Themeβ
Toggling document.documentElement.setAttribute('data-theme', 'light') overrides the surface and text tokens:
[data-theme="light"] {
--color-bg-primary: #f8fafc;
--color-bg-secondary: #f1f5f9;
--color-bg-surface: #ffffff;
--color-text-primary: #0f172a;
--color-text-secondary:#475569;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
}
Font Systemβ
Two typefaces are loaded from Google Fonts and assigned to specific semantic roles:
| Variable | Typeface | Usage |
|---|---|---|
--font-display | Plus Jakarta Sans | Page titles, section headings, navigation labels |
--font-body | Inter | Body copy, form inputs, data tables, badges |
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap');
:root {
--font-display: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-body: 'Inter', system-ui, sans-serif;
}
The display=swap parameter ensures text renders in the fallback font while the custom fonts load, preventing layout shift.
Component Inventoryβ
Page Components (routed by App)β
| Component | File | View ID | Description |
|---|---|---|---|
Landing | components/landing/Landing.jsx | β | Public-facing hero, features, problem statement, donate, CTA |
Dashboard | components/dashboard/Dashboard.jsx | dashboard | Stats overview, top voted, recent reports, city picker |
BrowseIssues | components/browse/BrowseIssues.jsx | browse | Unified issues + categories: sidebar, sort, advanced filters, My Issues |
Detail | components/issues/Detail.jsx | browse (overlay) | Full issue view β media, upvote, chat, status, resolution, assignment |
Events | components/events/Events.jsx | events | Community events β RSVP, filters, nominations, completion tracking |
SocialWall | components/social/SocialWall.jsx | social | Platform tabs: X + WhatsApp + Instagram (live feeds) + Facebook (coming soon) |
CityRankings | components/rankings/CityRankings.jsx | rankings | City rankings + contributor leaderboard, drilldown |
SupportPage | components/support/SupportPage.jsx | support | Donate, donor wall, How Can I Help, Upcoming Projects |
AdminPanel | components/admin/AdminPanel.jsx | admin | Feature flags, content overrides, reopens, flagged issues, users, accountability CRUD |
UserProfile | components/auth/UserProfile.jsx | profile | User identity, city editor, API token management |
Shared Components (components/shared/)β
| Component | File | Description |
|---|---|---|
FlagsCtx | FlagsCtx.js | React Context for feature flags + ICON_MAP side-effect |
Ic | Ic.jsx | Custom SVG icon renderer (21+ inline icons) |
ImageMetaPanel | ImageMetaPanel.jsx | EXIF metadata display panel |
FlagModal | FlagModal.jsx | Shared flag dialog for images and issues |
SignInModal | SignInModal.jsx | Guest sign-in prompt with mock Google accounts |
ShareModal | ShareModal.jsx | Social sharing dialog (WhatsApp, X, LinkedIn, Telegram, copy link) |
Issue Components (components/issues/)β
| Component | File | Description |
|---|---|---|
ICard | ICard.jsx | Issue card used in Browse, Dashboard, CityRankings drilldown |
NewModal | NewModal.jsx | Report issue form: title/description/city/category/priority/media/geo/ward/pincode |
ResolveModal | ResolveModal.jsx | Resolution workflow modal with proof media |
ReopenRequestInline | ReopenRequestInline.jsx | Inline reopen request form |
ChatPanel | ChatPanel.jsx | Real-time discussion panel with reactions and replies |
AssignmentPanel | AssignmentPanel.jsx | Hierarchical official chain (Ward Officer β Corporator β MLA β MP) with WhatsApp/X/Email deep-links |
WardSubmitModal | WardSubmitModal.jsx | Public form to submit ward official data |
AccountabilityMini | AccountabilityMini.jsx | Compact inline accountability chain with resolved official names |
Media Components (components/media/)β
| Component | File | Description |
|---|---|---|
Lightbox | Lightbox.jsx | Full-screen media viewer with keyboard nav and flagging |
UploadZone | UploadZone.jsx | Drag-and-drop file upload for images and videos |
MediaGallery | MediaGallery.jsx | Full gallery with All/Images/Videos tabs and flagging |
MediaStrip | MediaStrip.jsx | Compact thumbnail strip for cards |
Map Components (components/maps/)β
| Component | File | Description |
|---|---|---|
IssueDetailMap | IssueDetailMap.jsx | Single-marker Leaflet map for issue detail (exports MARKER_ICON) |
IssueClusterMap | IssueClusterMap.jsx | Clustered marker map for issue lists |
Browse Components (components/browse/)β
| Component | File | Description |
|---|---|---|
CategoryBrowser | CategoryBrowser.jsx | Category grid with status filtering and map toggle |
IssuesList | IssuesList.jsx | Sorted/filtered issues list with search |
Cities | Cities.jsx | City grid browser with per-city issue counts |
Social Components (components/social/)β
| Component | File | Description |
|---|---|---|
ConvertModal | ConvertModal.jsx | Tweet/WA/IG message β issue converter with media selection |
XFeed | XFeed.jsx | X/Twitter live mentions feed with convert buttons |
WAFeed | WAFeed.jsx | WhatsApp messages feed with convert buttons |
IGFeed | IGFeed.jsx | Instagram mentions feed with convert buttons |
Rankings Components (components/rankings/)β
| Component | File | Description |
|---|---|---|
ContributorCard | ContributorCard.jsx | Contributor row with rank, score bar, badges |
ContributorDetail | ContributorDetail.jsx | Expanded contributor view with score breakdown |
ContributorLeaderboard | ContributorLeaderboard.jsx | Contributor rankings list with sorting |
Leaderboard | Leaderboard.jsx | City leaderboard table (exports SCORE_SORT_OPTIONS) |
Event Components (components/events/)β
| Component | File | Description |
|---|---|---|
EventCard | EventCard.jsx | Event card with type badge, metadata, join button |
CreateEventModal | CreateEventModal.jsx | Event creation form with type selector |
EventDetail | EventDetail.jsx | Full event detail view with join CTA and attendees |
Support Components (components/support/)β
| Component | File | Description |
|---|---|---|
FeatureRequestModal | FeatureRequestModal.jsx | Feature request form (exports FR_CATS) |
JoinTeamModal | JoinTeamModal.jsx | Team application form (exports TEAM_ROLES) |
SupporterForm | SupporterForm.jsx | Supporter/donor registration form |
Admin Components (components/admin/)β
| Component | File | Description |
|---|---|---|
AdminPanel | AdminPanel.jsx | Main admin view β feature flags, content overrides, reopens, flagged issues, users, accountability tab |
AccountabilityAdmin | AccountabilityAdmin.jsx | CRUD interface for jurisdictions, officials, departments, and police stations with CSV import/export |
Hooks (hooks/)β
| Hook | File | Description |
|---|---|---|
useFlags | useFlags.js | Feature flag management with localStorage persistence |
useUserLocation | useUserLocation.js | Browser Geolocation API wrapper |
usePushNotifications | usePushNotifications.js | Web Push API subscription management |
useDonors | useDonors.js | Donor data fetching with mock fallback |
State Managementβ
The application uses React's built-in hooks. There is no Redux, Zustand, or MobX.
Patternβ
Each major view manages its own local state. Shared state (authenticated user, active city filter, unread notification count) lives in the top-level App component and is passed down as props.
function App() {
const [user, setUser] = useState(null);
const [currentView, setCurrentView] = useState('landing');
const [activeCity, setActiveCity] = useState('all');
const [notifications, setNotifications] = useState([]);
const [socket, setSocket] = useState(null);
// Socket.IO connection is established once on login
// and stored in state so all child components can use it
useEffect(() => {
if (!user) return;
const s = io(import.meta.env.VITE_WS_URL, {
auth: { token: user.accessToken },
});
setSocket(s);
return () => s.disconnect();
}, [user]);
return (
<SocketContext.Provider value={socket}>
{/* ... */}
</SocketContext.Provider>
);
}
Data Fetchingβ
Each view component fetches its own data using useEffect + fetch. A shared apiFetch helper attaches the Authorization header and handles token refresh:
async function apiFetch(path, options = {}) {
const token = sessionStorage.getItem('accessToken');
const res = await fetch(`${import.meta.env.VITE_API_URL}${path}`, {
...options,
credentials: 'include', // sends refresh cookie
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (res.status === 401) {
// attempt silent refresh
await refreshToken();
return apiFetch(path, options); // retry once
}
return res.json();
}
Real-Time Updates via Socket.IOβ
The Socket.IO client connects once the user authenticates. Components subscribe to events using useEffect cleanup:
function IssuesList({ socket }) {
const [issues, setIssues] = useState([]);
useEffect(() => {
if (!socket) return;
const onIssueCreated = (issue) => {
setIssues(prev => [issue, ...prev]);
};
const onIssueUpdated = (update) => {
setIssues(prev =>
prev.map(i => i.issueId === update.issueId ? { ...i, ...update } : i)
);
};
socket.on('issue:created', onIssueCreated);
socket.on('issue:updated', onIssueUpdated);
socket.on('issue:upvoted', onIssueUpdated);
return () => {
socket.off('issue:created', onIssueCreated);
socket.off('issue:updated', onIssueUpdated);
socket.off('issue:upvoted', onIssueUpdated);
};
}, [socket]);
// ...
}
See Real-Time Architecture for the full event catalogue.
Buildβ
cd frontend
npm run build
# Output: frontend/dist/
Vite produces a dist/ folder with hashed asset filenames suitable for deployment to S3 + CloudFront, Vercel, or any static host. See Deployment for instructions.
Multi-Page App (MPA) β Admin Portal Separate Bundleβ
The frontend is configured as a Vite MPA with two HTML entry points:
index.html β main app bundle (src/main.jsx β App.jsx β civic-pulse.jsx)
admin/index.html β admin bundle (src/admin-main.jsx β admin/AdminApp.jsx)
vite.config.js:
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
admin: resolve(__dirname, 'admin/index.html'),
},
},
},
The admin bundle uses BrowserRouter basename="/admin" so all admin routes live under /admin/**. The admin portal is never linked from the main app nav β it is only reachable via /admin directly or through the subtle link at the bottom of the sign-in card.