Live: weather.swayam.space
A fast, accessible weather dashboard built with React 19, Vite 8, and Tailwind CSS v4, powered by OpenWeatherMap and Mapbox GL.
- Overview
- Features
- Tech Stack
- Architecture
- Project Structure
- Getting Started
- Environment Variables
- Data Sources & APIs
- Testing
- Performance
- Deployment
- Contributing
- License
Mausam (Hindi: मौसम, meaning "weather") is a clean weather application that shows current conditions and an hourly forecast for any location on Earth. Users can search for any city using the geocoding search dialog, switch between metric and imperial units, and view weather data across eight interactive chart types, all rendered alongside an interactive Mapbox map that adapts to the active light/dark theme.
- Current Conditions: Temperature, feels-like, wind speed and direction, humidity, pressure, visibility, and cloud cover displayed in a structured card with live OpenWeatherMap weather icons
- Hourly Forecast: 5-day / 3-hour forecast from the OpenWeatherMap Forecast API, visualized across eight tabbed chart types:
- Overview (temperature range)
- Precipitation
- Wind speed
- Humidity
- Cloud cover
- Pressure
- Visibility
- Feels Like
- City Search: Debounced geocoding search (400 ms) with up to 5 results via OpenWeatherMap's Geocoding API, triggered by clicking the search button or pressing
Ctrl/Cmd + K - Mapbox GL Map: Interactive map centered on the selected location with a custom marker; map style automatically switches between
light-v11anddark-v11based on the active theme - Persistent Location: Last selected coordinates are persisted to
localStoragevia Zustand so the user's location is remembered across sessions
- Dark / Light Theme: A single click-to-switch toggle (no menu, no "system" option) with preference persisted to
localStorage - Metric / Imperial Toggle: Switch between °C / m/s and °F / mph at any time; unit preference is also persisted
- Dynamic Page Title: Browser tab title updates to reflect the current location and temperature
- Skip-to-Content Link: Hidden accessible link for keyboard and screen-reader navigation
- ARIA Labels: All interactive elements, loading states, and data regions carry descriptive
aria-*attributes - Skeleton Loading UI: Cards and charts display skeleton placeholders while data is in flight
- Error Boundaries: Per-section
AppErrorBoundarycomponents prevent a single component failure from crashing the entire page
| Layer | Technology |
|---|---|
| Framework | React 19 |
| Build Tool | Vite 8 |
| Language | TypeScript 5 |
| Styling | Tailwind CSS v4, shadcn/ui, Radix UI |
| Data Fetching | TanStack Query v5 (React Query) |
| HTTP Client | Axios |
| State Management | Zustand v5 (with persist middleware) |
| Charts | Recharts v3 |
| Map | Mapbox GL JS v3 |
| Icons | Lucide React |
| Typography | Geist Variable Font (@fontsource-variable/geist) |
| Linting | ESLint 9, eslint-plugin-react-hooks, eslint-plugin-react-refresh |
| Formatting | Prettier with prettier-plugin-tailwindcss |
| Unit/Component Testing | Vitest, React Testing Library |
| E2E Testing | Playwright |
| CI | GitHub Actions (lint, unit tests, typecheck, build, E2E on every PR) |
+----------------------------------------------------------------+
| React App (Vite) |
| |
| +---------------+ +----------------+ +---------------+ |
| | AppHeader | | CurrentWeather | | HourlyTabs | |
| | (Search, | | Card | | (8 charts, | |
| | Unit, Theme)| | | | lazy-loaded) | |
| +---------------+ +----------------+ +---------------+ |
| |
| +------------------------------------------------------------+ |
| | Map (Mapbox GL JS) | |
| | theme-aware, marker, pan on select | |
| +------------------------------------------------------------+ |
| |
| +------------------------------------------------------------+ |
| | Zustand Stores (persisted) | |
| | useWeatherStore (lat/lon), useUnitStore (unit) | |
| +---------------------------+----------------------------------+ |
| | |
| +---------------------------v----------------------------------+ |
| | TanStack Query - useWeatherQuery | |
| | queryKey: ["weather", lat, lon, unit] | |
| | staleTime: 5 min, retry: 1 | |
| +---------------------------+----------------------------------+ |
+------------------------------+-----------------------------------+
| axios (interceptors)
+----------------+----------------+
| OpenWeatherMap API |
| /data/2.5/weather |
| /data/2.5/forecast |
| /geo/1.0/reverse |
| /geo/1.0/direct (search) |
+--------------------------------------+
Data flow: Zustand stores hold the selected lat/lon and unit. useWeatherQuery constructs a TanStack Query keyed on those three values, firing three OpenWeatherMap requests in parallel (Promise.all). Components subscribe to the shared useWeather hook which composes the store and query together. All eight chart components are lazy-loaded via React.lazy and Suspense to keep the initial bundle small.
.
├── public/ # Static assets (icons, manifest, OG image)
├── src/
│ ├── api/
│ │ └── index.ts # Axios instance + response interceptors
│ ├── assets/
│ │ └── Logo.tsx # SVG logo component
│ ├── components/
│ │ ├── charts/ # Eight Recharts chart components (lazy-loaded)
│ │ │ ├── OverviewChart.tsx
│ │ │ ├── PrecipitationChart.tsx
│ │ │ ├── WindChart.tsx
│ │ │ ├── HumidityChart.tsx
│ │ │ ├── CloudCoverChart.tsx
│ │ │ ├── PressureChart.tsx
│ │ │ ├── VisibilityChart.tsx
│ │ │ └── FeelsLikeChart.tsx
│ │ ├── dialogs/
│ │ │ └── SearchDialog.tsx # City search with geocoding + Ctrl+K shortcut
│ │ ├── dropdowns/
│ │ │ ├── UnitToggle.tsx # Metric / Imperial switcher
│ │ │ └── theme-toggle.tsx # Click-to-switch dark/light toggle
│ │ ├── layout/
│ │ │ ├── AppHeader.tsx # Top navigation bar
│ │ │ ├── AppErrorBoundary.tsx
│ │ │ ├── Footer.tsx
│ │ │ └── PageHeader.tsx # Location name + sunrise/sunset display
│ │ ├── map/
│ │ │ ├── Map.tsx # Mapbox GL map, theme-aware style switching
│ │ │ └── Marker.tsx # Custom map marker
│ │ ├── providers/
│ │ │ ├── theme-provider.tsx # Theme provider component
│ │ │ └── use-theme.ts # Theme context + useTheme hook
│ │ ├── ui/ # shadcn/ui primitives (button, card, dialog, etc.)
│ │ └── weather/
│ │ ├── CurrentWeatherCard.tsx # Current conditions display
│ │ └── HourlyWeatherTabs.tsx # Tabbed chart panel
│ ├── config/
│ │ ├── app.ts # Unit symbols + localStorage keys
│ │ ├── weather.ts # API defaults (lat, lon, unit, search limit)
│ │ ├── mapbox.ts # Mapbox defaults (center, zoom)
│ │ └── index.ts # Re-exports
│ ├── features/
│ │ └── weather/
│ │ └── useWeatherQuery.ts # TanStack Query data-fetching hook
│ ├── hooks/
│ │ ├── useGeocoding.ts # Debounced city search hook
│ │ ├── usePageTitle.ts # Dynamic browser tab title
│ │ └── useWeather.ts # Composed hook (store + query)
│ ├── lib/
│ │ └── utils.ts # cn(), formatUnixTime(), helpers
│ ├── store/
│ │ ├── useUnitStore.ts # Persisted metric/imperial preference
│ │ └── useWeatherStore.ts # Persisted lat/lon selection
│ ├── index.css # Tailwind base + CSS custom properties
│ ├── test/
│ │ └── setup.ts # Vitest global setup (jest-dom, jsdom polyfills)
│ ├── types/
│ │ ├── common/ # Geo and timezone types
│ │ └── weather/ # Fully typed OWM API response shapes
│ ├── App.tsx # Root layout and section composition
│ └── main.tsx # React root + QueryClient setup
├── e2e/ # Playwright end-to-end tests
│ ├── fixtures/
│ │ └── mockWeatherApi.ts # Mocks the OpenWeatherMap endpoints
│ ├── app.spec.ts
│ ├── theme-toggle.spec.ts
│ ├── unit-toggle.spec.ts
│ └── search.spec.ts
├── vite.config.ts # Vite config with manual chunk splitting
├── vitest.config.ts # Unit/component test config
├── playwright.config.ts # E2E test config
├── TESTING.md # Full testing guide
├── tsconfig.app.json
└── package.json
- Node.js 20+
- An OpenWeatherMap API key (free tier sufficient)
- A Mapbox access token (free tier sufficient)
git clone https://github.com/swayamswarup/mausam.git
cd mausam
npm installnpm run devOpen http://localhost:5173.
npm run build
npm run preview # preview the production build locallynpm run build runs a full TypeScript check (tsc -b) before invoking Vite, so the build fails fast on type errors instead of shipping them silently.
Copy .env.example to .env.local in the project root and fill in your own keys:
cp .env.example .env.local# OpenWeatherMap - https://openweathermap.org/api
VITE_OPENWEATHER_API_KEY=your_openweathermap_api_key
# Mapbox - https://account.mapbox.com/
VITE_MAPBOX_TOKEN=your_mapbox_access_tokenAll
VITE_prefixed variables are inlined at build time by Vite and are visible in the browser bundle. Do not store server-side secrets here. Scope your Mapbox token to your production domain(s) in the Mapbox dashboard to prevent unauthorized use.
The Axios instance in src/api/index.ts automatically attaches VITE_OPENWEATHER_API_KEY as the appid query parameter on every request. A 401 response logs a clear setup message in the browser console pointing to the API key setup page.
Three endpoints are called in parallel on every location change:
| Endpoint | Purpose |
|---|---|
GET /data/2.5/weather |
Current weather conditions |
GET /data/2.5/forecast |
5-day / 3-hour hourly forecast |
GET /geo/1.0/reverse |
Reverse geocode coordinates to a location name |
GET /geo/1.0/direct |
Forward geocode search query to coordinates (search only) |
Data is cached by TanStack Query with a 5-minute staleTime. One retry is attempted on failure before surfacing an error state. If reverse geocoding returns no result (for example, coordinates over open ocean), the app falls back to a generic location label built from the raw coordinates instead of failing.
UV index and air quality are not currently implemented. OpenWeatherMap's free
/forecastendpoint doesn't include UV data; that requires the paid One Call API tier.
Used exclusively for the interactive map. The map style switches between mapbox://styles/mapbox/light-v11 and mapbox://styles/mapbox/dark-v11 based on the active theme, and smoothly re-centers on location changes.
The project has both a unit/component test suite (Vitest + React Testing Library) and an end-to-end test suite (Playwright, with the OpenWeatherMap API mocked so it doesn't need a real key). Both run in CI on every push and pull request.
npm test # unit/component tests, once
npm run test:watch # unit/component tests, watch mode
npm run test:coverage # unit/component tests with a coverage report
npm run test:e2e # end-to-end tests (requires: npx playwright install chromium, once)
npm run test:all # everything: coverage + E2ESee TESTING.md for the full guide, including what's covered, how to debug a failing test, and how to write new ones.
The Vite build applies several optimizations to keep load times fast:
- Manual chunk splitting: Four dedicated vendor chunks are emitted so browsers can cache them independently of app code:
vendor-react,vendor-mapbox,vendor-charts,vendor-query. Mapbox GL and Recharts are never bundled with the app entry point. - Lazy-loaded charts: All eight chart components use
React.lazy+Suspense. Chart code is only downloaded when the Hourly Forecast section is rendered, reducing the initial JS payload significantly. es2020build target: Modern syntax is preserved without unnecessary transpilation for supported browsers.- Hidden source maps: Production source maps are emitted but not referenced in the bundle, enabling error reporting without exposing source to end users.
Note: the Mapbox GL vendor chunk is still the largest chunk in the build (roughly 450 KB gzipped) since Mapbox GL JS itself is a large library. It's isolated to its own chunk so it doesn't block the initial app render or get re-downloaded on unrelated deploys.
The app is live at weather.swayam.space.
For self-hosted deployments, npm run build produces a fully static dist/ folder that can be served from any static host (Vercel, Netlify, Cloudflare Pages, NGINX, etc.). No server-side runtime is required.
Ensure both VITE_OPENWEATHER_API_KEY and VITE_MAPBOX_TOKEN are set as build-time environment variables on your hosting platform before running the build step.
- Fork the repository and create a feature branch (
git checkout -b feat/my-feature). - Run
npm run lint,npm test, andnpm run buildbefore opening a pull request. - Follow the existing code style. Prettier is configured with
prettier-plugin-tailwindcssand should be run on save.
Bug reports and feature requests are welcome via GitHub Issues.
MIT. See LICENSE for details.