Skip to main content

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 -b and bundled with Vite 5.
  • CSS Modules for all styling (*.module.css files colocated with components) — there is no Tailwind, styled-components, or global utility-class system in this app.
  • rollup-plugin-visualizer wired into the build to audit bundle weight (vite build writes dist/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.css file (import s from './FeedPage.module.css') and applies classes via className. The one sanctioned exception is src/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-property via the style prop — the property itself is still defined and consumed in the .module.css file. The helper's own comment states this is "the ONLY sanctioned use of the style prop in this codebase." In practice the rule is followed almost everywhere, though ApplyModal.tsx has two small raw style={{ background: 'var(--accent)' }} literals that bypass cssVars() — a minor, known inconsistency rather than a second sanctioned exception.
  • Path aliases. @/* resolves to src/*, configured in both vite.config.ts (resolve.alias) and tsconfig.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.tsx is wrapped in React.lazy(), and the router tree is wrapped in a single <Suspense> (inside AppLayout) with a PageLoader fallback, so each feature page ships as its own chunk.
  • Micro-components. Feature folders are broken into small, single-purpose components (a *Page.tsx that composes several smaller pieces) rather than one large page file — see the feature list below.
  • Manual vendor chunking. vite.config.ts splits react / react-dom and react-router-dom into their own manualChunks so they cache independently of app code.
  • ESLint + Prettier, wired together via eslint-config-prettier so 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.

PathPage componentFeature folder
/LandingPagelanding
/feedFeedPagefeed
/ticket/:idTicketPageticket
/mapWardMapPagemap
/scoresScoresPagescores
/administratorsAdministratorsPageadministrators
/reportersReportersPagereporters
/u/:slugProfilePagereporters
/dashboardReporterDashboardPagereporters
/reportReportPagereport
/eventsEventsPageevents
/events/:idEventDetailPageevents
/aboutAboutPageabout
/supportSupportPagesupport
/searchSearchPagesearch
*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 — see cityDefs.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 (IssueCard per issue), a small map teaser, and a right rail; backed by GET /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 by GET /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 from GET /wards and 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 by GET /scores/units and GET /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 by GET /administrators and GET /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 by GET /reporters, GET /reporters/:slug, and GET /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 /events and /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 by GET /nominations.
  • about — the static /about page: hero, how-it-works recap, stats band, "what we stand for," and the channels through which issues can be reported.
  • support — the /support help 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 by GET /search.

Data fetching

Feature pages read live data through two small helpers in src/lib/:

  • api.ts exports apiFetch<T>(path, opts), a thin fetch wrapper against the API base URL (import.meta.env.VITE_API_URL, defaulting to http://localhost:3001/api), and qs(params), a helper that builds a query string while skipping undefined/null/empty values.
  • useApi.ts exports useApi<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. Passing path: null skips 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.