Skip to content

Air-Intelligence/AIR_FE

Repository files navigation

AIR_FE

AIR Intelligence Frontend Application - Real-time Weather Information and Hazard Alert System

πŸ“‹ Table of Contents

πŸ›  Tech Stack

Core

  • React 19 - UI library
  • TypeScript - Type safety
  • Vite 7 - Build tool and development server
  • React Router DOM 7 - Client-side routing (createBrowserRouter pattern)

Styling & UI

  • Tailwind CSS 4 - Utility-first CSS framework
  • Radix UI - Accessible UI components (shadcn/ui pattern)
  • Lucide React - Icon library

Map & Location

  • Mapbox GL JS - Interactive map rendering
  • Turf.js - Geospatial data processing (polygon smoothing)
  • Geolocation API - Real-time user location tracking

HTTP & API

  • ky - Lightweight HTTP client (Fetch API based)

PWA & Notifications

  • Service Worker - Background push notifications
  • Web Push API - VAPID-based push subscriptions

πŸ“ Project Structure

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

🎯 Core Features

1. User Management

  • Auto User Creation: Generate userId from backend on first visit and store in localStorage
  • First-Visit Detection: RootGate component checks hasVisited and redirects to /welcome page

2. Real-time Location Tracking

  • 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

3. Warning Level System

  • WarningLevelContext:
    • 5 levels: SAFE, READY, WARNING, DANGER, RUN
    • Backend-calculated warning level managed as global state on location updates
    • Access via useWarningLevel hook in components

4. Interactive Map

  • 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)

5. Push Notifications

  • Service Worker (/serviceWorker.js):
    • VAPID key-based push subscriptions
    • Request notification permissions and send subscription info to backend
    • Receive background notifications

6. Onboarding

  • OnboardingModal:
    • Display usage guide on first home visit
    • Prevent re-display using hasSeenOnboarding localStorage flag
    • Reset available from InfoButton

πŸ— Architecture

Layer Structure

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     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    β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Flow

1. App Initialization (App.tsx)

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

2. First-Visit Flow (RootGate.tsx)

RootGate Render
  └─> localStorage.getItem("hasVisited")
        β”œβ”€ null: localStorage.setItem("hasVisited", "true")
        β”‚         └─> <Navigate to="/welcome" />
        └─ "true": <Outlet /> β†’ Render HomePage

3. HomePage Rendering (HomePage.tsx)

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' })

Core Component Interactions

Warning Level System

[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 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Map Layer Visibility Control

[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         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

API Communication

Base URL: VITE_PUBLIC_API_URL/api/v1

Endpoints:

  • POST /users - User creation
  • PUT /users/last-coord - Location update + warning level query
  • POST /notifications/subscribe - Save push subscription
  • GET /weathers/polygon - Weather polygon data
  • GET /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);
  }
}

State Management Strategy

1. localStorage (Persistent Storage)

  • userId: User identifier (UUID)
  • userLocation: Latest location info { lat, lng, error }
  • hasVisited: First-visit flag ("true" string)
  • hasSeenOnboarding: Onboarding completion flag ("true" string)

2. React Context (Global State)

// WarningLevelContext
interface WarningLevelContextValue {
  warningLevel: "SAFE" | "READY" | "WARNING" | "DANGER" | "RUN" | null;
}

// Usage
const { warningLevel } = useWarningLevel();

3. Custom Hooks (Logic Encapsulation)

  • useGeolocation(intervalMs): Location tracking + API calls
  • useCreateUser(): User creation/restoration
  • useWebPush(vapidKey): Service Worker registration
  • useMapBounds(): Mapbox boundary management

4. Local State (Component UI State)

  • Modal visibility (isOpen)
  • Zoom level (zoomLevel)
  • Timer countdown (secondsLeft)

Path Aliases

@/* β†’ src/* (configured in tsconfig.json and vite.config.ts)

Key Design Decisions

Why Location Updates Occur in Two Places

  1. useGeolocation: Location data collection + localStorage storage
  2. WarningLevelContext: Location-based warning level queries

β†’ useGeolocation is called inside the Context Provider for automatic integration

Zoom Level Threshold Selection (8)

  • Zoom > 8: Detailed polygon rendering (fine-grained weather visualization per region)
  • Zoom ≀ 8: Point markers (performance optimization for national/wide view)

Service Worker Error Recovery

  • On USER_NOT_FOUND: Invalidate localStorage userId + regenerate
  • On push subscription failure: Log error only (app functions normally)

πŸš€ Development Setup

Requirements

  • Node.js 20+
  • pnpm (corepack recommended)

Environment Variables

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

Installation and Running

# Install dependencies
pnpm install

# Start dev server (port 5173, network exposed)
pnpm dev

# Production build
pnpm build

# Preview build
pnpm preview

# Lint
pnpm lint

πŸ“¦ Deployment

Docker

Multi-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:latest

Key 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
  • /health health check endpoint

CI/CD (GitHub Actions)

Workflow: .github/workflows/pnpm-build-deploy.yml

Trigger: Push to main branch

Steps:

  1. Build Job:

    • Build Docker image (inject environment variables)
    • Push to Docker Hub: air-core-dev-fe-images:latest
  2. Deploy Job:

    • SSH to GCE
    • docker compose pull && up -d
    • Discord webhook notification (success/failure)

Required GitHub Secrets:

  • DOCKER_USERNAME, DOCKER_PASSWORD
  • GCE_HOST, GCE_USER, GCE_SSH_KEY
  • VITE_PUBLIC_MAPBOX_KEY, VITE_VAPID_PUBLIC_KEY, VITE_PUBLIC_API_URL
  • DISCORD_WEBHOOK_URL

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •