Project Overview — Public Site
The public site (apps/web) is the citizen-facing web app for CivicPulse India — the surface residents use
to browse civic issues, see who is accountable for their area, file a report, and take part in community
events. It is a single-page application (SPA): a Vite + React + TypeScript client that talks to the backend
API over HTTP and does not render on the server.
This page covers what the site is for, the real routes and feature areas as they exist in the repository today, and the frontend conventions the codebase follows. It does not cover the backend API contract or the admin app — see the other Project Overview pages for those.
Stack
From apps/web/package.json and the Vite/TypeScript config:
- React 18 with React Router v6 (
react-router-dom) for client-side routing. - TypeScript, compiled with
tsc -band bundled with Vite 5. - CSS Modules for all styling (
*.module.cssfiles colocated with components) — there is no Tailwind, styled-components, or global utility-class system in this app. rollup-plugin-visualizerwired into the build to audit bundle weight (vite buildwritesdist/stats.html).- No server-side rendering and no state-management library beyond React's own
useReducer— see Client-side state below.
Frontend conventions
These conventions are enforced in the actual source, not just documented:
- CSS Modules only, no inline styles. Every styled component imports its own
*.module.cssfile (import s from './FeedPage.module.css') and applies classes viaclassName. The one sanctioned exception issrc/lib/cssVars.ts, a small helper that bridges a runtime-only value (a color, a width percentage, a computed background) into a CSS Module as a--custom-propertyvia thestyleprop — the property itself is still defined and consumed in the.module.cssfile. The helper's own comment states this is "the ONLY sanctioned use of thestyleprop in this codebase." In practice the rule is followed almost everywhere, thoughApplyModal.tsxhas two small rawstyle={{ background: 'var(--accent)' }}literals that bypasscssVars()— a minor, known inconsistency rather than a second sanctioned exception. - Path aliases.
@/*resolves tosrc/*, configured in bothvite.config.ts(resolve.alias) andtsconfig.json(compilerOptions.paths). Imports use@/components/ui,@/lib/api,@/data, etc. instead of long relative paths. - Route-based lazy loading. Every top-level page component in
src/App.tsxis wrapped inReact.lazy(), and the router tree is wrapped in a single<Suspense>(insideAppLayout) with aPageLoaderfallback, so each feature page ships as its own chunk. - Micro-components. Feature folders are broken into small, single-purpose components (a
*Page.tsxthat composes several smaller pieces) rather than one large page file — see the feature list below. - Manual vendor chunking.
vite.config.tssplitsreact/react-domandreact-router-dominto their ownmanualChunksso they cache independently of app code. - ESLint + Prettier, wired together via
eslint-config-prettierso formatting rules don't fight Prettier. Notable rules:@typescript-eslint/no-explicit-any(warn),@typescript-eslint/no-unused-vars(error, with^_ignore pattern), and the React Hooks recommended rule set.
Routing
Routes are declared in src/App.tsx. All routes render inside a shared AppLayout (top bar, sidebar,
toast/lightbox/apply-modal overlays). Any unmatched path redirects to /feed.
| Path | Page component | Feature folder |
|---|---|---|
/ | LandingPage | landing |
/feed | FeedPage | feed |
/ticket/:id | TicketPage | ticket |
/map | WardMapPage | map |
/scores | ScoresPage | scores |
/administrators | AdministratorsPage | administrators |
/reporters | ReportersPage | reporters |
/u/:slug | ProfilePage | reporters |
/dashboard | ReporterDashboardPage | reporters |
/report | ReportPage | report |
/events | EventsPage | events |
/events/:id | EventDetailPage | events |
/about | AboutPage | about |
/support | SupportPage | support |
/search | SearchPage | search |
* | redirects to /feed | — |
The sidebar is hidden on the landing page (/) only; every other route shows the full app shell.
Feature folders
src/features/ has one folder per feature area, each holding its page component plus the smaller
components that make it up (and their .module.css files). As of this writing the folders are:
landing— the marketing/entry page at/: hero section, a city picker (currently Mumbai-only — seecityDefs.ts), a "how it works" explainer, and a closing call-to-action footer.feed— the live issue feed at/feed: a searchable, filterable list of reported civic issues (IssueCardper issue), a small map teaser, and a right rail; backed byGET /issues.ticket— the single-issue detail page at/ticket/:id: media gallery, a small location map, the "chain of responsibility" (which office/role is accountable), an activity/status timeline, and an actions sidebar (vote, flag, close/reopen); backed byGET /issues/:id.map— the interactive ward map at/map: a schematic (not geographically accurate) layout of all 24 BMC (Brihanmumbai Municipal Corporation) wards plus the suburban rail lines, with a drill-down panel per ward; ward scores are fetched fromGET /wardsand merged onto a static schematic layout. "BMC" is Mumbai's municipal governing body.scores— the accountability scoreboard at/scores: a citywide score hero and ward leaderboard, plus a drill-down by ward or by pincode with issue mix, the Corporator → MLA → MP accountability chain, and a downloadable report (PDF/CSV); backed byGET /scores/unitsandGET /scores/report.administrators— the "know your administrators" directory at/administrators: elected and administrative offices accountable for civic services, browsable by role (Corporator, MLA, MP, etc.) and searchable by jurisdiction; deliberately lists offices, not named individuals; backed byGET /administratorsandGET /administrators/roles.reporters— three routes: the reporters index (/reporters, filterable by zone), a public reporter profile (/u/:slug) that visitors can follow, and the dashboard for a fixed demo reporter (/dashboard, no real auth/session exists on the public site yet) with their report history and a leaderboard; backed byGET /reporters,GET /reporters/:slug, andGET /issues.report— the "report an issue" flow at/report: a multi-step form (category, media upload, description, location) plus a sidebar showing how the report will be routed, ending in a success card.events— community events at/eventsand/events/:id: citizens/NGOs create and join events (clean-ups, plantation drives, awareness walks, etc.), filterable by type and city, plus a "nominated activities" section (citizen-flagged ideas seeking an organiser) backed byGET /nominations.about— the static/aboutpage: hero, how-it-works recap, stats band, "what we stand for," and the channels through which issues can be reported.support— the/supporthelp page: contact methods, an emergency notice, and an FAQ list.search— the global search page at/search: a single query fans out across issues, reporters, wards, and events, backed byGET /search.
Data fetching
Feature pages read live data through two small helpers in src/lib/:
api.tsexportsapiFetch<T>(path, opts), a thinfetchwrapper against the API base URL (import.meta.env.VITE_API_URL, defaulting tohttp://localhost:3001/api), andqs(params), a helper that builds a query string while skippingundefined/null/empty values.useApi.tsexportsuseApi<T>(path, deps), a fetch-on-mount-and-on-deps-change hook that returns{ data, loading, error }. It tracks a request sequence number internally so that a stale, slow response (e.g. from a fast-typed search box) can't overwrite a newer one. Passingpath: nullskips the fetch entirely — used when a required param (like a route slug) isn't ready yet.
Per the comment in api.ts, this hook-based fetching is meant to replace an earlier prototype that read
from static arrays in src/data/; that src/data/ module still exists and is used for local reference
data and seed/demo values (e.g. avatarPal, the avatar-color palette used across roughly nine files), not
as the source of live issue, reporter, or score data. Not everything in src/data/ is still wired up,
though — landingCityDefs (in src/data/civic.ts) has no import sites anywhere and is effectively dead
code.
Client-side state
src/store/AppStore.tsx provides a small global store (React Context + useReducer) for state that is
genuinely shared across the app: the current user, per-issue vote/flag toggles, per-reporter follow
toggles, the lightbox and "apply to become a reporter" modal, toast messages, and the joined/created events
list. Everything else (search query, active filter, form fields, etc.) is local useState inside the
owning component, per the project's "isolate local state, use global stores only for genuinely shared
data" rule.
Civic events are fetched from the API into the store on mount. Voting, flagging, and closing/reopening a ticket, following a reporter, and joining or creating an event, however, are all handled entirely in the reducer as in-memory client state — as of this writing there is no corresponding write endpoint being called for those actions, so they do not persist across a page reload or sync to other users.