Skip to content

Technigo Final Project: BADA - Real-time beach water quality web app with maps & favorites#136

Open
govargas wants to merge 247 commits into
Technigo:mainfrom
govargas:main
Open

Technigo Final Project: BADA - Real-time beach water quality web app with maps & favorites#136
govargas wants to merge 247 commits into
Technigo:mainfrom
govargas:main

Conversation

@govargas

@govargas govargas commented Nov 3, 2025

Copy link
Copy Markdown

🎓 Web dev spring 2025 Final Project Submission

This is my final project for the Technigo Fullstack JavaScript Bootcamp. BADA is a web application that helps people in Sweden find safe beaches with real-time water quality data.

🎯 What I Built

A full-stack application featuring:

  • Frontend: React + TypeScript, MapLibre maps, i18next multi-language support
  • Backend: Express API with MongoDB, JWT authentication, caching layer
  • Features: Interactive beach map, geolocation, favorites with drag-drop sorting, dark mode
  • Lighthouse Scores: 100% Accessibility, SEO, and Best Practices

🚀 Key Achievements

  • Integrated official Swedish Agency for Marine and Water Management (Havs- och vatten­myndigheten) API with intelligent caching
  • Built custom React hooks for geolocation, dark mode, and outside-click detection
  • Implemented server-side caching to reduce API load and improve performance
  • Achieved full accessibility compliance (WCAG standards)
  • Created seamless UX with search autocomplete, toast notifications, and smooth animations
  • Deployed frontend (Netlify) and backend (Vercel) with CI/CD

💡 Technical Highlights

  • Type Safety: Full TypeScript implementation across frontend and backend
  • State Management: Zustand for global state, TanStack Query for server state
  • Performance: Optimized API calls with intelligent caching and query invalidation
  • Accessibility: Keyboard navigation, ARIA labels, screen reader support
  • UX Polish: Dark mode, i18n (Swedish/English), error boundaries, loading states

🎨 Design Decisions

  • Mobile-first responsive design with Tailwind CSS
  • Clean, minimal, easy to read and navigate UI
  • Consistent design tokens for colors, spacing, and typography
  • Dark mode that respects system preferences

📚 What I Learned

Building BADA taught me how to architect a real-world full-stack application, integrate external APIs, implement authentication, and focus on user experience and accessibility. I'm proud of the result and I'm looking forward to read your feedback!


Live Demo: https://badaweb.netlify.app/
Test Account: smoke@test.com / Test1234

See README.md for full documentation and setup instructions.

Split the duplicated useDarkMode into two clearly named hooks:
- useToggleDarkMode: writes the .dark class (used in Header)
- useDarkModeObserver: reads/observes the .dark class via MutationObserver (used in AmbientBackground)
govargas added 5 commits June 8, 2026 11:14
- WeatherData type: temperature, feelsLike, uvIndex, waterTemperature
- SunTimes type: sunrise/sunset, solar noon, golden hour bounds, all twilight phases
- fetchBeachWeather: calls Open-Meteo forecast + marine in parallel, waterTemperature is null if marine API has no coverage
- fetchSunTimes: calls sunrise-sunset.org with formatted=0 for ISO timestamps; golden hour derived as ±1hr from sunrise/sunset
- useWeather: 30-min staleTime (weather changes)
- useSunTimes: 12-hr staleTime keyed by date (sun times are stable)

Note: during Nordic summers nautical/astronomical twilight times return 1970-01-01 epoch from the API — UI layer should treat that as "does not occur".
WeatherCard: air temperature + feels like (large °C), water temperature
from Open-Meteo marine, UV index with color-coded level label.

SunCard: Helios-inspired gradient arc bar showing the day from civil
twilight to civil twilight, with golden-hour zones highlighted in amber
and a live "now" dot tracking the current sun position. Sunrise/sunset
time labels sit below the bar. Golden hour morning/evening windows in
an amber callout, civil and nautical twilight rows (nautical hidden
during Nordic summer when the API returns epoch), day length footer.

Both cards use skeleton loaders while fetching and graceful error
states. Panel is bilingual (sv/en) via i18next keys in both locales.
feat: weather and sun times on beach detail page
govargas and others added 22 commits June 8, 2026 17:54
- Add weather & sun times panel to features list (Open-Meteo + sunrise-sunset.org)
- Replace useDarkMode with useToggleDarkMode/useDarkModeObserver in hooks section
- Add useWeather and useSunTimes hooks
- Add Open-Meteo and sunrise-sunset.org to external APIs
- Move weather integration from Planned to Completed in roadmap
The diagnostic endpoint exposed env-var presence flags and the
configured CORS origins to anyone. It was only ever meant as a
temporary deploy check; removing it.
A hanging HaV upstream could previously pin a serverless function open
until the platform timeout. AbortSignal.timeout caps each call at 5s.
The beach-list feature endpoint is now cached for an hour since the
list of EU beaches changes only a few times a year.
The key was committed inline in MapView and in a public repo. Now it
comes from VITE_MAPTILER_KEY; the style IDs (not secret) stay in source.
NOTE: rotate the old key in the MapTiler dashboard and add an
allowed-origin restriction — the old value lives on in git history.
When /api/beaches errored, data was undefined and isLoading false (React
Query uses isFetching on refetch), so the effect called refetch() on
every render — an infinite request storm exactly when the backend was
already failing. React Query's built-in retry already covers this; the
error UI with a manual retry button remains.
Showing '59.3326, 18.0649' is noise for the everyday user; the distance
badge and municipality already convey location. Coordinates remain
available on the map and detail view.
Click/hover handlers and the source+layers were created inside the
points effect, which re-ran on every map move (each move changes the
filtered points). That piled up duplicate listeners (N popups per click)
and leaked memory, and the isStyleLoaded() guard meant markers sometimes
never rendered when data arrived before the style.

Now the source and layers are built once on load (and re-added after a
theme/style swap, which setStyle wipes), handlers are registered once,
and the points effect only calls source.setData().
The panel previously showed only current conditions, which doesn't help
someone planning a swim later in the week. Open-Meteo returns a daily
forecast from the same endpoint at no extra cost, so this pulls
max/min temp, max UV and precipitation probability for the next 5 days
and renders a compact strip under the current conditions.
The dissuasion field was already in the BeachDetail type but never
rendered. It carries active swimming advisories — the app's whole point
— so it now appears as a prominent alert banner above the meta card when
present.
Search is now a real ARIA combobox (role=combobox, aria-expanded,
aria-controls, aria-autocomplete, aria-activedescendant) with full
keyboard support: ArrowUp/Down move the active option, Enter selects,
Escape closes. Options no longer wrap a <button> (invalid inside
role=option) and the 'view all' link sits outside the listbox.

The hamburger and user dropdowns were role=menu/menuitem but never
implemented arrow-key navigation, which the menu role requires. They are
navigation links, so the roles are removed and normal tab order applies.
The first page load went client -> serverless (cold start) -> HaV ->
large GeoJSON every time, and the in-memory cache is useless on
serverless. The beach list barely changes, so a build script now
snapshots it to frontend/public/beaches.json (2632 beaches, ~280KB),
served from the CDN with no cold start. fetchBeaches reads the snapshot
and falls back to the live proxy only if it is missing. A scheduled
GitHub Action refreshes the snapshot daily and commits changes.
Each route now sets a meaningful document.title (the beach name on the
detail page), and the build script emits sitemap.xml covering the static
pages plus all 2632 beach URLs. robots.txt points at it. This is the
SEO that a no-SSR SPA can realistically get; per-beach Open Graph still
needs prerendering (noted as a known limitation).
maplibre-gl was bundled into main.js (~1.4MB). A manualChunks split
drops main to ~500KB and isolates maplibre into a separately cacheable
chunk that loads in parallel, improving first-load and repeat-visit
caching.
Documents the conscious MVP trade-offs (per-instance cache, seasonal
data, approximate golden hour, client-side third-party APIs, token in
localStorage, no SSR) up front, and rewords the keyboard-navigation
claim to describe what is actually implemented rather than a blanket
'full support'.
Pre-mortem hardening: security, resilience, a11y, SEO & perf
HaV sends the string "false" (or empty) when no EU motive is recorded,
which leaked through to the UI as literal 'EU-motivering: false'. A small
cleanText helper now maps false/true/empty to no-data ('—') for both the
EU motive and algal status fields, in both languages.
Audit found the water-quality classification colors (the app's most
important data) failed AA contrast in both themes (2.0-4.3:1, need 4.5).
Retuned the quality tokens and added dark-theme overrides so every
classification, plus the UV high/extreme accents, now clears 4.5:1 on
both badge and card backgrounds. Also gave the landing page a
visually-hidden h1 — every other page had one but the main page did not.
…ontrast

- three-tier glass material system (header / cards / overlays) with edge
  highlights, hue-tinted shadows, and prefers-reduced-transparency fallback
- consolidate 4 font families to 2 (Zalando Sans + Special Gothic),
  self-hosted via Fontsource; drop the Google Fonts link
- weather panel hierarchy: water temperature leads at display size
- class-based dark: variant; it silently followed the OS media query
  while the app toggles a .dark class, breaking all dark: utilities
- WCAG AA fixes: primary buttons, language switcher, error text, and
  form inputs in dark mode
- replace text glyphs with Phosphor icons; document the radius scale
- add 404 page and og:image + twitter:image meta
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants