Technigo Final Project: BADA - Real-time beach water quality web app with maps & favorites#136
Open
govargas wants to merge 247 commits into
Open
Technigo Final Project: BADA - Real-time beach water quality web app with maps & favorites#136govargas wants to merge 247 commits into
govargas wants to merge 247 commits into
Conversation
…updated to node 22
…d switch to z.treeifyError
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)
- 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.
…els, hour reference marks
feat: weather and sun times on beach detail page
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎓 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:
🚀 Key Achievements
💡 Technical Highlights
🎨 Design Decisions
📚 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.