AIR Intelligence Frontend Application - Real-time Weather Information and Hazard Alert System
- React 19 - UI library
- TypeScript - Type safety
- Vite 7 - Build tool and development server
- React Router DOM 7 - Client-side routing (
createBrowserRouterpattern)
- Tailwind CSS 4 - Utility-first CSS framework
- Radix UI - Accessible UI components (shadcn/ui pattern)
- Lucide React - Icon library
- Mapbox GL JS - Interactive map rendering
- Turf.js - Geospatial data processing (polygon smoothing)
- Geolocation API - Real-time user location tracking
- ky - Lightweight HTTP client (Fetch API based)
- Service Worker - Background push notifications
- Web Push API - VAPID-based push subscriptions
src/
├── api/ # API client layer
│ ├── user.ts # User creation, coordinate updates, warning levels
│ ├── weather.ts # Weather data (polygon/point)
│ └── push.ts # Push notification subscriptions
├── app/ # App initialization and routing
│ ├── App.tsx # Root component (useCreateUser, useWebPush)
│ ├── RootGate.tsx # First-visit detection and welcome page redirect
│ └── router/
│ └── AppRouter.tsx # React Router configuration
├── components/ # Reusable UI components
│ ├── OnboardingModal.tsx # First-time user guide
│ ├── WarningButton.tsx # Warning level display button
│ ├── TimerTrigger.tsx # Timer trigger button
│ ├── PolygonLayer.tsx # Mapbox polygon layer
│ ├── PointLayer.tsx # Mapbox point layer
│ └── ui/ # shadcn/ui base components
├── context/ # React Context state management
│ └── warningLevelContext.tsx # Warning level global state
├── hooks/ # Custom hooks
│ ├── useCreateUser.ts # Generate userId on first visit and store in localStorage
│ ├── useGeolocation.ts # Real-time location tracking (interval-based)
│ └── useWebPush.ts # Service Worker registration and push subscriptions
├── lib/ # Library configuration
│ ├── ky.ts # HTTP client (baseURL, timeout, retry)
│ └── utils.ts # Utility functions (cn, etc.)
├── page/ # Page components
│ ├── home/
│ │ └── HomePage.tsx # Mapbox map + real-time location marker
│ └── welcome/
│ └── WelcomePage.tsx # First-visit welcome page
└── types/ # Type definitions
└── api/
└── common.ts
- Auto User Creation: Generate
userIdfrom backend on first visit and store in localStorage - First-Visit Detection:
RootGatecomponent checkshasVisitedand redirects to/welcomepage
- Geolocation Hook (
useGeolocation):- Configurable interval tracking (default 5 seconds)
- Store location in localStorage
- Update coordinates via backend API (
userApi.updateLastCoord) - Receive warning level (
warningLevel) response
- WarningLevelContext:
- 5 levels:
SAFE,READY,WARNING,DANGER,RUN - Backend-calculated warning level managed as global state on location updates
- Access via
useWarningLevelhook in components
- 5 levels:
- Mapbox GL JS based:
- Real-time user location marker updates
- Layer visibility control based on zoom level
- Zoom > 7: Display polygon layer
- Zoom ≤ 7: Display point layer
- Weather data visualization (GeoJSON polygons/points)
- Service Worker (
/serviceWorker.js):- VAPID key-based push subscriptions
- Request notification permissions and send subscription info to backend
- Receive background notifications
- OnboardingModal:
- Display usage guide on first home visit
- Prevent re-display using
hasSeenOnboardinglocalStorage flag - Reset available from InfoButton
┌─────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Pages │ │ Components │ │ Modals │ │
│ │ - HomePage │ │ - MapLayers │ │ - Warning │ │
│ │ - Welcome │ │ - Buttons │ │ - Tutorial │ │
│ └─────────────┘ └──────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ State Layer │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ React Context │ │ Custom Hooks │ │
│ │ - WarningLevel │ │ - useGeolocation │ │
│ │ │ │ - useCreateUser │ │
│ │ │ │ - useWebPush │ │
│ └─────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Data Layer │
│ ┌──────────────┐ ┌────────────────┐ ┌────────────┐ │
│ │ API Client │ │ localStorage │ │ Service │ │
│ │ (ky-based) │ │ - userId │ │ Worker │ │
│ │ │ │ - location │ │ - Push │ │
│ └──────────────┘ └────────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────┘
App Mount
├─> useCreateUser
│ └─> localStorage.getItem("userId")
│ ├─ If exists: Keep existing user
│ └─ If not: userApi.createUser() → store in localStorage
│
├─> useWebPush
│ └─> navigator.serviceWorker.register("/serviceWorker.js")
│ └─> Notification.requestPermission()
│ └─> pushManager.subscribe(VAPID_KEY)
│ └─> pushApi.saveSubscription()
│
└─> WarningLevelProvider initialization
└─> useGeolocation (5-second interval)
└─> navigator.geolocation.getCurrentPosition()
├─> localStorage.setItem("userLocation")
└─> userApi.updateLastCoord({ lat, lng })
└─> Response: { warningLevel: "SAFE" | "READY" | ... }
└─> setWarningLevel() → Update Context
RootGate Render
└─> localStorage.getItem("hasVisited")
├─ null: localStorage.setItem("hasVisited", "true")
│ └─> <Navigate to="/welcome" />
└─ "true": <Outlet /> → Render HomePage
HomePage Mount
├─> Initialize Mapbox
│ ├─> mapboxgl.Map({ center: [126.978, 37.5665], zoom: 7 })
│ ├─> Add ScaleControl
│ └─> Register zoom/moveend event listeners
│
├─> OnboardingModal (on first home visit)
│ └─> localStorage.getItem("hasSeenOnboarding")
│ └─ null: Display 3-step tutorial
│
├─> User location marker
│ └─> useGeolocation → detect { lat, lng } changes
│ └─> markerRef.setLngLat([lng, lat])
│
├─> WarningButton
│ └─> useWarningLevel() → subscribe to warningLevel
│ └─> Auto-display WarningModal on SAFE → !SAFE change
│
├─> TimerTrigger
│ └─> Countdown every second (independent timer)
│
└─> Map layers (zoom level-based visibility control)
├─> zoomLevel > 8: Display PolygonLayer
│ └─> weatherApi.getPolygon() → GeoJSON
│ └─> map.addLayer({ type: 'fill' })
│
└─> zoomLevel ≤ 8: Display PointLayer
└─> weatherApi.getPoints() → GeoJSON
└─> map.addLayer({ type: 'circle' })
[User Movement]
↓
useGeolocation (every 5 seconds)
↓
userApi.updateLastCoord({ lat, lng })
↓
Backend Response: { warningLevel: "WARNING" }
↓
WarningLevelContext.setWarningLevel("WARNING")
↓
┌─────────────────────────────────────┐
│ Components subscribing via │
│ useWarningLevel() │
├─────────────────────────────────────┤
│ 1. WarningButton │
│ └─> prevLevel "SAFE" → "WARNING" │
│ └─> Auto-display WarningModal │
│ │
│ 2. HomePage │
│ └─> Pass warningLevel prop │
│ └─> Change WarningButton color │
└─────────────────────────────────────┘
[User Zoom In/Out]
↓
map.on('zoom') event
↓
setZoomLevel(map.getZoom())
↓
useEffect([zoomLevel]) trigger
↓
┌─────────────────────────────────┐
│ zoomLevel > 8 │
│ ├─> PolygonLayer: visible │
│ └─> PointLayer: none │
│ │
│ zoomLevel ≤ 8 │
│ ├─> PolygonLayer: none │
│ └─> PointLayer: visible │
└─────────────────────────────────┘
Base URL: VITE_PUBLIC_API_URL/api/v1
Endpoints:
POST /users- User creationPUT /users/last-coord- Location update + warning level queryPOST /notifications/subscribe- Save push subscriptionGET /weathers/polygon- Weather polygon dataGET /weathers/point- Weather point data
Configuration (src/lib/ky.ts):
- Timeout: 10 seconds
- Retry: 2 attempts
- Headers:
Content-Type: application/json
Error Handling:
// useWebPush.ts
try {
await pushApi.saveSubscription({ endpoint, keys });
} catch (err) {
if (err.response.errorName === "USER_NOT_FOUND") {
// Regenerate userId
localStorage.removeItem("userId");
const newUser = await userApi.createUser();
localStorage.setItem("userId", newUser.content.userId);
}
}userId: User identifier (UUID)userLocation: Latest location info{ lat, lng, error }hasVisited: First-visit flag ("true"string)hasSeenOnboarding: Onboarding completion flag ("true"string)
// WarningLevelContext
interface WarningLevelContextValue {
warningLevel: "SAFE" | "READY" | "WARNING" | "DANGER" | "RUN" | null;
}
// Usage
const { warningLevel } = useWarningLevel();useGeolocation(intervalMs): Location tracking + API callsuseCreateUser(): User creation/restorationuseWebPush(vapidKey): Service Worker registrationuseMapBounds(): Mapbox boundary management
- Modal visibility (
isOpen) - Zoom level (
zoomLevel) - Timer countdown (
secondsLeft)
@/* → src/* (configured in tsconfig.json and vite.config.ts)
- useGeolocation: Location data collection + localStorage storage
- WarningLevelContext: Location-based warning level queries
→ useGeolocation is called inside the Context Provider for automatic integration
- Zoom > 8: Detailed polygon rendering (fine-grained weather visualization per region)
- Zoom ≤ 8: Point markers (performance optimization for national/wide view)
- On
USER_NOT_FOUND: Invalidate localStorage userId + regenerate - On push subscription failure: Log error only (app functions normally)
- Node.js 20+
- pnpm (corepack recommended)
Create a .env file:
VITE_PUBLIC_API_URL=http://localhost:8080
VITE_VAPID_PUBLIC_KEY=your-vapid-public-key
VITE_PUBLIC_MAPBOX_KEY=your-mapbox-token # TODO: Currently hardcoded in HomePage.tsx# Install dependencies
pnpm install
# Start dev server (port 5173, network exposed)
pnpm dev
# Production build
pnpm build
# Preview build
pnpm preview
# Lint
pnpm lintMulti-stage build (Node 20 Alpine → Nginx Alpine):
docker build \
--build-arg VITE_PUBLIC_API_URL=https://api.example.com \
--build-arg VITE_VAPID_PUBLIC_KEY=your-key \
--build-arg VITE_PUBLIC_MAPBOX_KEY=your-token \
-t air-fe:latest .
docker run -p 3000:80 air-fe:latestKey Features:
- pnpm usage (frozen-lockfile)
- TypeScript type check skipped (build speed optimization)
- Nginx static file serving
- Service Worker support
- SPA routing (
try_files) - gzip compression, security headers
/healthhealth check endpoint
Workflow: .github/workflows/pnpm-build-deploy.yml
Trigger: Push to main branch
Steps:
-
Build Job:
- Build Docker image (inject environment variables)
- Push to Docker Hub:
air-core-dev-fe-images:latest
-
Deploy Job:
- SSH to GCE
docker compose pull && up -d- Discord webhook notification (success/failure)
Required GitHub Secrets:
DOCKER_USERNAME,DOCKER_PASSWORDGCE_HOST,GCE_USER,GCE_SSH_KEYVITE_PUBLIC_MAPBOX_KEY,VITE_VAPID_PUBLIC_KEY,VITE_PUBLIC_API_URLDISCORD_WEBHOOK_URL