Frontend for school connectivity visualization and optimal tower network planning in Mozambique. Client: GIGA (UNICEF–ITU) · Developer: MOAI Analytics · License: MIT
git clone <repository-url>
cd giga-maps
npm install
npm run dev # → http://localhost:5173Default login: admin / giga2026
- Tech Stack
- Project Structure
- Architecture
- Data Layer
- Development Workflow
- Scripts Reference
- Testing
- Adding a New Layer
- Build & Deployment
- Docker
- Code Conventions
- License
MOAI | GIGA - Mozambique enables decision-makers to visualize and prioritize school connectivity investments by rendering multi-dimensional geospatial indices — climate risk, social vulnerability, infrastructure access — on an interactive WebGL map. The platform calculates optimal tower placement using configurable priority weights and K-values (50 / 200 / 400 total towers to install).
| Category | Detail |
|---|---|
| Interactive Map | WebGL rendering via MapLibre GL with dark/light/streets base styles |
| Priority Index System | Configurable weights across Climate, Social, and Access axes (levels Low / Medium / High) |
| Tower Network Optimization | K=50, K=200, and K=400 total towers to install with Haversine distance calculations |
| Multi-filter Panel | Filter schools by connectivity status, electricity availability, and education level |
| Index Selector | Threshold slider (0–100) with color gradient visualization |
| Layer Management | Toggle individual layers or groups with parent-child hierarchy |
| Rich Tooltips | School detail cards with all index values and color-coded indicators |
| Responsive Design | Optimized breakpoints for desktop (1920px), laptop (1366px), tablet (1024px), and mobile (768px–360px) |
| Authentication | Simple login gate with localStorage persistence |
| Layer | Technology | Version |
|---|---|---|
| Framework | React | 19.2.0 |
| Language | TypeScript | 5.9.3 (strict) |
| Build Tool | Vite | 7.x |
| Map Engine | MapLibre GL JS | 5.17.0 |
| Styling | Tailwind CSS | 4.1.18 |
| Icons | react-icons | 5.5.0 |
| Testing | Vitest + React Testing Library | 4.x / 16.x |
| Linting | ESLint (flat config) | 9.x |
| Formatting | Prettier | 3.x |
Node.js ≥ 18 and npm ≥ 9 required.
giga-maps/
├── public/
│ ├── data/
│ │ ├── schools_climate_index.geojson # Climate risk index per school
│ │ ├── schools_social_index.geojson # Social vulnerability index per school
│ │ ├── schools_access_index.geojson # Infrastructure access index per school
│ │ ├── towers_existing.geojson # Existing tower locations
│ │ ├── priority_school/ # 8 school priority GeoJSON variants
│ │ └── priority_tower/ # 27 tower placement GeoJSON variants (K50 + K200 + K400)
│ ├── icons/ # SVG marker icons (house.svg)
│ └── img/ # Logos: GIGA, UNICEF, ITU, MOAI
├── src/
│ ├── components/
│ │ ├── Map/
│ │ │ └── MapContainer.tsx # Core map: layers, icons, interactions
│ │ └── UI/
│ │ ├── Sidebar.tsx # Layer toggle panel with priority/K-value selectors
│ │ ├── ConnectivityFilter.tsx # Multi-section filter (connectivity, electricity, education)
│ │ ├── FilterPanel.tsx # Connectivity status toggle buttons (Yes/No/Unknown)
│ │ ├── Slider.tsx # Index threshold slider (0–100)
│ │ ├── MapControls.tsx # Zoom, fit-bounds, style toggle
│ │ ├── LoginModal.tsx # Authentication modal
│ │ ├── LogoutConfirmModal.tsx # Two-step logout confirmation
│ │ ├── LoaderModal.tsx # Loading overlay with spinner
│ │ ├── InfoModal.tsx # Help and usage documentation modal
│ │ ├── PrioritySelector.tsx # Priority level selector (Low / Medium / High)
│ │ ├── KValueSelector.tsx # Tower K-value selector (50/200/400)
│ │ └── ConnectedSchoolsBanner.tsx # Real-time connected school count banner
│ ├── hooks/
│ │ └── useGeoJSON.ts # Async GeoJSON loader with validation
│ ├── types/
│ │ └── geojson.ts # SchoolProperties, LayerConfig, FilterState, etc.
│ ├── test/ # Vitest test suite (27 tests)
│ ├── App.tsx # Root component with auth gating
│ ├── main.tsx # React 19 entry point
│ └── index.css # Global styles + responsive breakpoints
├── Dockerfile # Multi-stage production image
├── vite.config.ts # Build + test config
├── tsconfig.json # TypeScript project references
├── eslint.config.js # ESLint flat config
└── .prettierrc # Prettier config
src/
├── components/
│ ├── Map/
│ │ └── MapContainer.tsx # Core map component — layers, icons, interactions
│ └── UI/
│ ├── Sidebar.tsx # Layer toggle panel with priority/K-value selectors
│ ├── ConnectivityFilter.tsx # Multi-section filter (connectivity, electricity, education)
│ ├── FilterPanel.tsx # Connectivity status toggle buttons
│ ├── Slider.tsx # Heat index threshold slider
│ ├── Tooltip.tsx # School detail popup
│ ├── MapControls.tsx # Zoom, fit-bounds, style toggle
│ ├── LoginModal.tsx # Authentication modal
│ ├── LoaderModal.tsx # Loading overlay
│ ├── PrioritySelector.tsx # Priority level selector (Low / Medium / High)
│ └── KValueSelector.tsx # Tower K-value selector (50/200/400)
├── hooks/
│ └── useGeoJSON.ts # Async GeoJSON loader with validation
├── types/
│ └── geojson.ts # SchoolProperties, LayerConfig, FilterState
├── App.tsx # Root component with auth gating
├── main.tsx # Entry point
└── index.css # Global styles + responsive breakpoints
node --version # ≥ 18
npm --version # ≥ 9npm install
npm run devnpm run typecheck # TypeScript — must pass with 0 errors
npm run lint # ESLint — 0 errors (35 known warnings, threshold 40)
npm run format:check # Prettier — all files must be formatted
npm test # Vitest — 27/27 must passOr run all at once:
npm run typecheck && npm run lint && npm run format:check && npm test| Command | Description |
|---|---|
npm run dev |
Start Vite dev server (HMR) |
npm run build |
TypeScript check + production build |
npm run preview |
Serve production build locally |
npm run typecheck |
Run tsc --noEmit (no output, just type errors) |
npm run lint |
ESLint with --max-warnings 40 (35 known warnings within threshold) |
npm run lint:fix |
Auto-fix ESLint issues |
npm run format |
Prettier write over src/**/*.{ts,tsx,css,json} |
npm run format:check |
Check formatting without modifying files |
npm test |
Vitest single run |
npm run test:watch |
Vitest interactive watch mode |
npm run test:coverage |
Coverage report (v8, lcov output) |
Tests live in src/test/. Run with Vitest in jsdom environment.
giga-maps/
├── public/
│ ├── data/
│ │ ├── priority_school/ # School priority GeoJSON variants
│ │ ├── priority_tower/ # Tower placement GeoJSON variants (K50 / K200 / K400)
│ │ ├── schools_climate_index.geojson
│ │ ├── schools_social_index.geojson
│ │ ├── schools_access_index.geojson
│ │ └── towers_existing.geojson
│ ├── icons/ # SVG icons (house, tower)
│ └── img/ # Logos and favicon
├── src/ # Application source (see Architecture)
├── eslint.config.js # ESLint flat config
├── .prettierrc # Prettier configuration
├── vite.config.ts # Vite build config with production optimizations
├── tsconfig.json # TypeScript project references
├── Dockerfile # Multi-stage production build
├── .dockerignore # Docker build exclusions
└── package.json
All geospatial data is served as static GeoJSON files from public/data/.
| File | Description |
|---|---|
schools_climate_index.geojson |
Climate risk index per school |
schools_social_index.geojson |
Social vulnerability index per school |
schools_access_index.geojson |
Infrastructure access index per school |
towers_existing.geojson |
Existing tower locations |
Generated from combinations of parent axes — climate (cli), social (soc), access (acc) — and their sub-indices, each weighted at Low (1), Medium (2), or High (3). Sibling axes use _ as separator; parent-to-child groups use -:
- School Priority:
priority_school/schools_priority_index-cli{w}_soc{w}_acc{w}[-children...].geojson - Tower Optimal Placement:
priority_tower/optimal_towers_K{50|200|400}-cli{w}_soc{w}_acc{w}[-children...].geojson
Sub-indices appear only when their parent reaches High (3): ext, geo, sho, hyd for Climate; pov, nut for Social; hea, pow, roa for Access.
Constraint rules:
- Exactly one High is required among parents at all times; the only exception is the all-Medium state (2, 2, 2).
- Parents: Setting Low is blocked (with a warning banner) unless another parent is already at High.
- Sub-indices: Setting Low always succeeds; if no sibling is High, the system auto-promotes the first available sibling to High.
- Removing the last High (→ Medium) auto-resets any Lows to Medium (both parents and sub-indices).
Each school feature includes: School Name, School Giga ID, connectivity, electricity, education_level, num_students, PRIORITY_IDX (0–100 composite score), CLIM_IDX, SOC_IDX, ACC_IDX, and individual sub-index fields.
The application is deployed on Netlify with automatic builds from the main branch.
Live URL: https://moai-giga.netlify.app/
npm run build
# Output: dist/Build applies:
- Terser minification with
console.loganddebuggerremoval - Vendor chunk splitting:
vendor(React/ReactDOM),maplibre(MapLibre GL) - Source maps disabled
noindex, nofollowmeta tag (internal tool — not for public indexing)
Deployed on Netlify from the master branch. Configuration in netlify.toml:
- Build command:
npm run build, publish directory:dist/ - SPA redirect rule (
/* → /index.html) - GeoJSON served as
application/json→ triggers automatic gzip/Brotli compression on the CDN - Security headers:
X-Frame-Options,X-Content-Type-Options,Referrer-Policy
Live URL: https://moai-giga.netlify.app/
# Build
docker build -t giga-maps .
# Run
docker run -p 8080:80 giga-maps
# → http://localhost:8080Multi-stage build: node:20-alpine (build) → nginx:alpine (serve). SPA fallback routing configured via try_files.
- TypeScript strict mode — no
any, no unused variables (enforced by ESLint) - Prettier — single quotes, semicolons, trailing commas, print width 100, LF line endings
- Component files — one default export per file, named same as file
- No global state — all state local to
MapContainer.tsxpassed down as props - GeoJSON files excluded from Prettier (large files, formatter excluded via
.prettierignore) - Comments in English only
- No
console.login production code (Terser drops them, but keep source clean)
MIT — Copyright © 2025–2026 MOAI Analytics · GIGA UNICEF–ITU