Skip to main content

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 App component and flows via props/callbacks (Context/Redux is a separate future effort)
  • CSS unchanged β€” all classes from civic-pulse.css remain globally available; no CSS modules or scoping added
  • Side-effect import β€” FlagsCtx.js must be imported before any component that reads .Icon on 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:

VariableTypefaceUsage
--font-displayPlus Jakarta SansPage titles, section headings, navigation labels
--font-bodyInterBody 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)​

ComponentFileView IDDescription
Landingcomponents/landing/Landing.jsxβ€”Public-facing hero, features, problem statement, donate, CTA
Dashboardcomponents/dashboard/Dashboard.jsxdashboardStats overview, top voted, recent reports, city picker
BrowseIssuescomponents/browse/BrowseIssues.jsxbrowseUnified issues + categories: sidebar, sort, advanced filters, My Issues
Detailcomponents/issues/Detail.jsxbrowse (overlay)Full issue view β€” media, upvote, chat, status, resolution, assignment
Eventscomponents/events/Events.jsxeventsCommunity events β€” RSVP, filters, nominations, completion tracking
SocialWallcomponents/social/SocialWall.jsxsocialPlatform tabs: X + WhatsApp + Instagram (live feeds) + Facebook (coming soon)
CityRankingscomponents/rankings/CityRankings.jsxrankingsCity rankings + contributor leaderboard, drilldown
SupportPagecomponents/support/SupportPage.jsxsupportDonate, donor wall, How Can I Help, Upcoming Projects
AdminPanelcomponents/admin/AdminPanel.jsxadminFeature flags, content overrides, reopens, flagged issues, users, accountability CRUD
UserProfilecomponents/auth/UserProfile.jsxprofileUser identity, city editor, API token management

Shared Components (components/shared/)​

ComponentFileDescription
FlagsCtxFlagsCtx.jsReact Context for feature flags + ICON_MAP side-effect
IcIc.jsxCustom SVG icon renderer (21+ inline icons)
ImageMetaPanelImageMetaPanel.jsxEXIF metadata display panel
FlagModalFlagModal.jsxShared flag dialog for images and issues
SignInModalSignInModal.jsxGuest sign-in prompt with mock Google accounts
ShareModalShareModal.jsxSocial sharing dialog (WhatsApp, X, LinkedIn, Telegram, copy link)

Issue Components (components/issues/)​

ComponentFileDescription
ICardICard.jsxIssue card used in Browse, Dashboard, CityRankings drilldown
NewModalNewModal.jsxReport issue form: title/description/city/category/priority/media/geo/ward/pincode
ResolveModalResolveModal.jsxResolution workflow modal with proof media
ReopenRequestInlineReopenRequestInline.jsxInline reopen request form
ChatPanelChatPanel.jsxReal-time discussion panel with reactions and replies
AssignmentPanelAssignmentPanel.jsxHierarchical official chain (Ward Officer β†’ Corporator β†’ MLA β†’ MP) with WhatsApp/X/Email deep-links
WardSubmitModalWardSubmitModal.jsxPublic form to submit ward official data
AccountabilityMiniAccountabilityMini.jsxCompact inline accountability chain with resolved official names

Media Components (components/media/)​

ComponentFileDescription
LightboxLightbox.jsxFull-screen media viewer with keyboard nav and flagging
UploadZoneUploadZone.jsxDrag-and-drop file upload for images and videos
MediaGalleryMediaGallery.jsxFull gallery with All/Images/Videos tabs and flagging
MediaStripMediaStrip.jsxCompact thumbnail strip for cards

Map Components (components/maps/)​

ComponentFileDescription
IssueDetailMapIssueDetailMap.jsxSingle-marker Leaflet map for issue detail (exports MARKER_ICON)
IssueClusterMapIssueClusterMap.jsxClustered marker map for issue lists

Browse Components (components/browse/)​

ComponentFileDescription
CategoryBrowserCategoryBrowser.jsxCategory grid with status filtering and map toggle
IssuesListIssuesList.jsxSorted/filtered issues list with search
CitiesCities.jsxCity grid browser with per-city issue counts

Social Components (components/social/)​

ComponentFileDescription
ConvertModalConvertModal.jsxTweet/WA/IG message β†’ issue converter with media selection
XFeedXFeed.jsxX/Twitter live mentions feed with convert buttons
WAFeedWAFeed.jsxWhatsApp messages feed with convert buttons
IGFeedIGFeed.jsxInstagram mentions feed with convert buttons

Rankings Components (components/rankings/)​

ComponentFileDescription
ContributorCardContributorCard.jsxContributor row with rank, score bar, badges
ContributorDetailContributorDetail.jsxExpanded contributor view with score breakdown
ContributorLeaderboardContributorLeaderboard.jsxContributor rankings list with sorting
LeaderboardLeaderboard.jsxCity leaderboard table (exports SCORE_SORT_OPTIONS)

Event Components (components/events/)​

ComponentFileDescription
EventCardEventCard.jsxEvent card with type badge, metadata, join button
CreateEventModalCreateEventModal.jsxEvent creation form with type selector
EventDetailEventDetail.jsxFull event detail view with join CTA and attendees

Support Components (components/support/)​

ComponentFileDescription
FeatureRequestModalFeatureRequestModal.jsxFeature request form (exports FR_CATS)
JoinTeamModalJoinTeamModal.jsxTeam application form (exports TEAM_ROLES)
SupporterFormSupporterForm.jsxSupporter/donor registration form

Admin Components (components/admin/)​

ComponentFileDescription
AdminPanelAdminPanel.jsxMain admin view β€” feature flags, content overrides, reopens, flagged issues, users, accountability tab
AccountabilityAdminAccountabilityAdmin.jsxCRUD interface for jurisdictions, officials, departments, and police stations with CSV import/export

Hooks (hooks/)​

HookFileDescription
useFlagsuseFlags.jsFeature flag management with localStorage persistence
useUserLocationuseUserLocation.jsBrowser Geolocation API wrapper
usePushNotificationsusePushNotifications.jsWeb Push API subscription management
useDonorsuseDonors.jsDonor 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.