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