-
-
Couldn't load subscription status.
- Fork 6
feat: Integrate Windy.com Webcam API #289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This change integrates the Windy.com Webcam API into the application. It adds a new "Webcams" mode to the map, which displays webcams from the Windy.com API as markers on the map. Clicking on a marker shows a preview of the webcam stream. The following changes were made: - Added a new `WINDY_API_KEY` environment variable. - Created a new `lib/windy.ts` file to fetch webcam data. - Added a `WebcamMode` to the `MapToggleEnum`. - Added a "Webcams" option to the map toggle menu with a `Tent` icon. - Modified the `mapbox-map.tsx` component to fetch and display webcams.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds a new Sensor mode: environment keys for WINDY_API_KEY, a sensors data module (Windy Webcams API client), UI controls (SensorToggle + map toggle enum), Mapbox rendering of per-sensor DOM markers with popups and lifecycle management, and replaces TentTree UI buttons with the SensorToggle in header and mobile bar. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant UI as SensorToggle / Header / MobileBar
participant MT as MapToggleContext
participant MM as MapboxMap
participant LS as lib/sensors.getSensors
participant WA as Windy Webcams API
U->>UI: Click "Sensors"
UI->>MT: setMapType(SensorMode)
MT->>MM: notify mode change -> SensorMode
MM->>MM: clear existing markers/state
MM->>LS: getSensors()
LS->>LS: validate WINDY_API_KEY
LS->>WA: GET /webcams (x-windy-api-key)
WA-->>LS: JSON (webcams[]) / error
alt Success
LS-->>MM: WindyResponse (webcams[])
MM->>MM: create DOM markers + attach Popups (preview + link)
U-->>MM: interact with markers/popups
else Error
LS-->>MM: throw/error
MM->>MM: show toast/log error
end
rect rgba(230,245,255,0.9)
note right of MM: On mode switch or unmount -> remove all sensor markers and reset state
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
.env(1 hunks).env.local.example(1 hunks)components/map-toggle-context.tsx(1 hunks)components/map-toggle.tsx(2 hunks)components/map/mapbox-map.tsx(4 hunks)lib/windy.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
components/map/mapbox-map.tsx (1)
lib/windy.ts (2)
Webcam(1-16)getWebcams(22-40)
components/map-toggle.tsx (1)
components/ui/dropdown-menu.tsx (1)
DropdownMenuItem(188-188)
🪛 dotenv-linter (3.3.0)
.env
[warning] 2-2: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
.env.local.example
[warning] 7-7: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
🪛 Biome (2.1.2)
components/map-toggle.tsx
[error] 11-11: Do not shadow the global "Map" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
🔇 Additional comments (2)
components/map-toggle-context.tsx (1)
9-10: Enum addition looks good; confirm no persisted numeric dependencies.If enum values are stored (e.g., localStorage/DB) as numbers, adding members can break assumptions. You added at the end, so ordinals remain stable—please verify no persistence relies on numeric ordinals.
lib/windy.ts (1)
28-40: Windy API: use X-WINDY-API-KEY, filter by bbox/viewport, and validate tokenized images.current.preview
- Endpoint is correct: GET https://api.windy.com/webcams/api/v3/webcams; auth header must be X-WINDY-API-KEY.
- images.current.preview is returned as a direct (tokenized, expiring) URL — no extra "show" param is required for preview; still check response.images?.current?.preview at runtime and handle token expiry (do not long-cache).
- Use ?bbox=north_lat,east_lon,south_lat,west_lon to limit results for a viewport, or /map/clusters with northLat/eastLon/southLat/westLon+zoom for clustering/tiling.
- Change suggestions (lib/windy.ts lines 28–40): send header 'X-WINDY-API-KEY', append bbox when calling from a map/viewport, add runtime null-checks for images.current.preview, and account for token expiry in caching/refresh logic.
Likely an incorrect or invalid review comment.
| @@ -1 +1,2 @@ | |||
| DATABASE_URL="postgresql://user:password@host:port/db" | |||
| WINDY_API_KEY="YOUR_WINDY_API_KEY" | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Do not quote placeholder values; keep style consistent with dotenv-linter.
Unquote to satisfy the linter and match common dotenv style.
-WINDY_API_KEY="YOUR_WINDY_API_KEY"
+WINDY_API_KEY=YOUR_WINDY_API_KEY📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| WINDY_API_KEY="YOUR_WINDY_API_KEY" | |
| WINDY_API_KEY=YOUR_WINDY_API_KEY |
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 2-2: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
🤖 Prompt for AI Agents
In .env around line 2, the placeholder value for WINDY_API_KEY is quoted which
violates the dotenv-linter style; remove the surrounding double quotes so the
line reads with an unquoted placeholder (no extra spaces or trailing characters)
and save/commit the change to keep style consistent.
| # Windy.com Webcams API Key | ||
| WINDY_API_KEY="your_windy_api_key_here" | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Example value: drop quotes to appease dotenv-linter (optional).
Not required, but aligns with the linter and house style.
-# Windy.com Webcams API Key
-WINDY_API_KEY="your_windy_api_key_here"
+# Windy.com Webcams API Key
+WINDY_API_KEY=your_windy_api_key_here📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Windy.com Webcams API Key | |
| WINDY_API_KEY="your_windy_api_key_here" | |
| # Windy.com Webcams API Key | |
| WINDY_API_KEY=your_windy_api_key_here |
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 7-7: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
🤖 Prompt for AI Agents
In .env.local.example around lines 6 to 8, the example WINDY_API_KEY value is
quoted which can trigger dotenv-linter and doesn't match house style; update the
example by removing the surrounding double quotes so the line reads
WINDY_API_KEY=your_windy_api_key_here (no quotes) and ensure there is a newline
after the entry.
components/map-toggle.tsx
Outdated
| DropdownMenuTrigger | ||
| } from '@/components/ui/dropdown-menu' | ||
| import { Map, Pencil } from 'lucide-react' | ||
| import { Map, Pencil, Tent } from 'lucide-react' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid shadowing global Map; rename import.
Prevents lints and confusion with the global Map constructor.
-import { Map, Pencil, Tent } from 'lucide-react'
+import { Map as MapIcon, Pencil, Tent } from 'lucide-react'-<Map className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
+<MapIcon className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { Map, Pencil, Tent } from 'lucide-react' | |
| import { Map as MapIcon, Pencil, Tent } from 'lucide-react' | |
| <MapIcon className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> |
🧰 Tools
🪛 Biome (2.1.2)
[error] 11-11: Do not shadow the global "Map" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
🤖 Prompt for AI Agents
In components/map-toggle.tsx around line 11, the named import "Map" shadows the
global Map constructor; rename the import (e.g., import { Map as MapIcon,
Pencil, Tent } from 'lucide-react') and update all uses of the component in this
file to the new identifier (MapIcon) to avoid linting errors and confusion with
the global Map.
components/map/mapbox-map.tsx
Outdated
| import { getWebcams, Webcam } from '@/lib/windy'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Do not import server-only Windy client in a client component. Switch to your API route.
Replace the import and use the API route added in lib/windy.ts review.
-import { getWebcams, Webcam } from '@/lib/windy';
+// Fetch from server API route to avoid exposing the secret
+type Webcam = {
+ id: string;
+ title: string;
+ location: { latitude: number; longitude: number };
+ images?: { current?: { preview?: string } };
+};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { getWebcams, Webcam } from '@/lib/windy'; | |
| // Fetch from server API route to avoid exposing the secret | |
| type Webcam = { | |
| id: string; | |
| title: string; | |
| location: { latitude: number; longitude: number }; | |
| images?: { current?: { preview?: string } }; | |
| }; |
🤖 Prompt for AI Agents
In components/map/mapbox-map.tsx around lines 14-15, the file imports the
server-only Windy client (getWebcams, Webcam) which must not be used in a client
component; remove that import and instead call the API route you added in
lib/windy.ts from the client. Replace the import with a client-side data fetch
(e.g., fetch('/api/windy' or the specific endpoint you created) inside an effect
or async handler), parse the JSON into Webcam objects, and set component state
or pass the data via props; ensure proper loading/error handling and typings for
the fetched Webcam array.
components/map/mapbox-map.tsx
Outdated
| const [webcams, setWebcams] = useState<Webcam[]>([]); | ||
| const webcamMarkersRef = useRef<{ [id: string]: mapboxgl.Marker }>({}); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
OK to track markers in a ref. Add unmount cleanup to avoid leaks.
Also clear markers on component unmount in the main cleanup.
Add to existing unmount cleanup:
Object.values(webcamMarkersRef.current).forEach(m => m.remove());
webcamMarkersRef.current = {};🤖 Prompt for AI Agents
In components/map/mapbox-map.tsx around lines 36 to 38, webcamMarkersRef is used
to track mapbox markers but there is no removal on component unmount; add to the
existing cleanup (useEffect return) logic that iterates
Object.values(webcamMarkersRef.current) and calls remove() on each marker, then
reset webcamMarkersRef.current to an empty object to prevent memory leaks and
dangling DOM elements.
| useEffect(() => { | ||
| if (map.current && mapType === MapToggleEnum.WebcamMode) { | ||
| // Clear existing markers | ||
| Object.values(webcamMarkersRef.current).forEach(marker => marker.remove()); | ||
| webcamMarkersRef.current = {}; | ||
|
|
||
| webcams.forEach(webcam => { | ||
| const el = document.createElement('div'); | ||
| el.className = 'webcam-marker'; | ||
| el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/web-camera.png")'; | ||
| el.style.width = '25px'; | ||
| el.style.height = '25px'; | ||
| el.style.backgroundSize = 'cover'; | ||
| el.style.borderRadius = '50%'; | ||
| el.style.cursor = 'pointer'; | ||
|
|
||
| const marker = new mapboxgl.Marker(el) | ||
| .setLngLat([webcam.location.longitude, webcam.location.latitude]) | ||
| .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML( | ||
| `<h3>${webcam.title}</h3><img src="${webcam.images.current.preview}" alt="${webcam.title}" style="width:100%;" />` | ||
| )) | ||
| .addTo(map.current!); | ||
|
|
||
| webcamMarkersRef.current[webcam.id] = marker; | ||
| }); | ||
| } | ||
| }, [webcams, mapType]); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Avoid setHTML; sanitize or build DOM to prevent XSS.
Do not inject unsanitized title/URLs into HTML. Use setDOMContent with created nodes.
- webcams.forEach(webcam => {
+ webcams.forEach(webcam => {
const el = document.createElement('div');
el.className = 'webcam-marker';
el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/web-camera.png")';
el.style.width = '25px';
el.style.height = '25px';
el.style.backgroundSize = 'cover';
el.style.borderRadius = '50%';
el.style.cursor = 'pointer';
- const marker = new mapboxgl.Marker(el)
+ const popupRoot = document.createElement('div');
+ const h3 = document.createElement('h3');
+ h3.textContent = webcam.title ?? 'Webcam';
+ const img = document.createElement('img');
+ const preview = webcam.images?.current?.preview;
+ if (preview) {
+ img.src = preview;
+ img.alt = webcam.title ?? 'Webcam preview';
+ img.style.width = '100%';
+ popupRoot.append(h3, img);
+ } else {
+ popupRoot.append(h3);
+ }
+
+ const marker = new mapboxgl.Marker(el)
.setLngLat([webcam.location.longitude, webcam.location.latitude])
- .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML(
- `<h3>${webcam.title}</h3><img src="${webcam.images.current.preview}" alt="${webcam.title}" style="width:100%;" />`
- ))
+ .setPopup(new mapboxgl.Popup({ offset: 25 }).setDOMContent(popupRoot))
.addTo(map.current!);
webcamMarkersRef.current[webcam.id] = marker;
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (map.current && mapType === MapToggleEnum.WebcamMode) { | |
| // Clear existing markers | |
| Object.values(webcamMarkersRef.current).forEach(marker => marker.remove()); | |
| webcamMarkersRef.current = {}; | |
| webcams.forEach(webcam => { | |
| const el = document.createElement('div'); | |
| el.className = 'webcam-marker'; | |
| el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/web-camera.png")'; | |
| el.style.width = '25px'; | |
| el.style.height = '25px'; | |
| el.style.backgroundSize = 'cover'; | |
| el.style.borderRadius = '50%'; | |
| el.style.cursor = 'pointer'; | |
| const marker = new mapboxgl.Marker(el) | |
| .setLngLat([webcam.location.longitude, webcam.location.latitude]) | |
| .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML( | |
| `<h3>${webcam.title}</h3><img src="${webcam.images.current.preview}" alt="${webcam.title}" style="width:100%;" />` | |
| )) | |
| .addTo(map.current!); | |
| webcamMarkersRef.current[webcam.id] = marker; | |
| }); | |
| } | |
| }, [webcams, mapType]); | |
| useEffect(() => { | |
| if (map.current && mapType === MapToggleEnum.WebcamMode) { | |
| // Clear existing markers | |
| Object.values(webcamMarkersRef.current).forEach(marker => marker.remove()); | |
| webcamMarkersRef.current = {}; | |
| webcams.forEach(webcam => { | |
| const el = document.createElement('div'); | |
| el.className = 'webcam-marker'; | |
| el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/web-camera.png")'; | |
| el.style.width = '25px'; | |
| el.style.height = '25px'; | |
| el.style.backgroundSize = 'cover'; | |
| el.style.borderRadius = '50%'; | |
| el.style.cursor = 'pointer'; | |
| const popupRoot = document.createElement('div'); | |
| const h3 = document.createElement('h3'); | |
| h3.textContent = webcam.title ?? 'Webcam'; | |
| const img = document.createElement('img'); | |
| const preview = webcam.images?.current?.preview; | |
| if (preview) { | |
| img.src = preview; | |
| img.alt = webcam.title ?? 'Webcam preview'; | |
| img.style.width = '100%'; | |
| popupRoot.append(h3, img); | |
| } else { | |
| popupRoot.append(h3); | |
| } | |
| const marker = new mapboxgl.Marker(el) | |
| .setLngLat([webcam.location.longitude, webcam.location.latitude]) | |
| .setPopup(new mapboxgl.Popup({ offset: 25 }).setDOMContent(popupRoot)) | |
| .addTo(map.current!); | |
| webcamMarkersRef.current[webcam.id] = marker; | |
| }); | |
| } | |
| }, [webcams, mapType]); |
🤖 Prompt for AI Agents
In components/map/mapbox-map.tsx around lines 532 to 559, the code uses setHTML
on the Popup with unsanitized webcam.title and image URL which risks XSS;
replace setHTML with setDOMContent by creating a DOM fragment: build a container
element, create and append an h3 with textContent = webcam.title, create an img
element with src set to a validated/safe URL (or use a whitelist/check and
default placeholder on failure) and alt set via textContent assignment, set
styles via element.style (no innerHTML), then call new mapboxgl.Popup({ offset:
25 }).setDOMContent(container). Also ensure you validate webcam.id and other
fields before using them and keep using textContent for any text to prevent HTML
injection.
| const el = document.createElement('div'); | ||
| el.className = 'webcam-marker'; | ||
| el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/web-camera.png")'; | ||
| el.style.width = '25px'; | ||
| el.style.height = '25px'; | ||
| el.style.backgroundSize = 'cover'; | ||
| el.style.borderRadius = '50%'; | ||
| el.style.cursor = 'pointer'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
External icon URL for marker is brittle; prefer local asset or inline SVG.
Prevents third-party dependency, CSP issues, and mixed-content surprises.
🤖 Prompt for AI Agents
components/map/mapbox-map.tsx around lines 539 to 547: the marker currently uses
an external icon URL which is brittle; replace that with a local asset or an
inline SVG data URL. Either import a local PNG/SVG (e.g., import webcamIcon from
'../../public/icons/webcam.svg') and set el.style.backgroundImage =
`url(${webcamIcon})`, or embed an inline SVG by setting el.style.backgroundImage
= `url("data:image/svg+xml;utf8,${encodeURIComponent('<svg ...>...</svg>')}")`
or by inserting SVG markup directly into the element (el.innerHTML = '<svg
.../>') and adjust width/height/backgroundSize/borderRadius accordingly; ensure
your bundler/Next.js config supports importing the asset if you choose the
import approach.
lib/windy.ts
Outdated
| export async function getWebcams(): Promise<WindyResponse> { | ||
| const apiKey = process.env.WINDY_API_KEY; | ||
| if (!apiKey) { | ||
| throw new Error('WINDY_API_KEY is not defined in the environment variables.'); | ||
| } | ||
|
|
||
| const response = await fetch('https://api.windy.com/webcams/api/v3/webcams', { | ||
| method: 'GET', | ||
| headers: { | ||
| 'x-windy-api-key': apiKey, | ||
| }, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Client-side secret exposure and runtime undefined env. Move to server-only + proxy.
This module is imported by a client component (mapbox-map.tsx). In Next.js, process.env.WINDY_API_KEY is not exposed to the browser (will be undefined), and making requests with the key from the client would leak the secret. Serve via a server-only module or API route.
Apply server-only guard here (prevents client import):
+import 'server-only';Create an API route (example: app/api/webcams/route.ts):
// app/api/webcams/route.ts
import 'server-only';
export const runtime = 'nodejs'; // or 'edge' if Windy allows
export async function GET(req: Request) {
const apiKey = process.env.WINDY_API_KEY;
if (!apiKey) return new Response('Missing WINDY_API_KEY', { status: 500 });
const { searchParams } = new URL(req.url);
const bbox = searchParams.get('bbox'); // "minLon,minLat,maxLon,maxLat" optional
const url = new URL('https://api.windy.com/webcams/api/v3/webcams');
if (bbox) url.searchParams.set('bbox', bbox);
// TODO: add show= fields if required by API to include images
const windyResp = await fetch(url.toString(), {
headers: { 'x-windy-api-key': apiKey },
cache: 'no-store',
});
if (!windyResp.ok) {
const text = await windyResp.text();
return new Response(`Windy error: ${windyResp.status} ${text}`, { status: 502 });
}
return new Response(await windyResp.text(), { headers: { 'content-type': 'application/json' } });
}Then have the client call /api/webcams instead of using the key directly. See suggested diffs in mapbox-map.tsx comments.
🤖 Prompt for AI Agents
In lib/windy.ts around lines 22-34, the module currently reads
process.env.WINDY_API_KEY and performs the Windy API fetch but is imported by a
client component which would either see undefined at runtime or (if bundled)
leak the secret; stop allowing client imports by making this module server-only
(e.g., add a server-only guard / move it out of client import paths) and remove
direct usage from client code. Fix by extracting the Windy fetch logic into a
server-only API route (e.g., app/api/webcams/route.ts) that reads
process.env.WINDY_API_KEY, validates it, forwards client query params (like
bbox) to the Windy endpoint, returns the Windy response (handling non-OK
statuses), and uses server runtime caching policy; then update the client
(mapbox-map.tsx) to call the new API route (e.g., /api/webcams?bbox=...) instead
of importing lib/windy.ts or using the key directly.
This commit fixes an issue where the webcam previews were not displaying. The Windy.com API requires that all webcam images are linked back to their respective webcam pages on Windy.com. This was not being done, which caused the images to not load. The following changes were made: - Updated the `Webcam` interface in `lib/windy.ts` to include the `urls` property, which contains the URL to the webcam page. - Updated the popup HTML in `components/map/mapbox-map.tsx` to wrap the webcam preview image in a link to the webcam page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
♻️ Duplicate comments (5)
lib/windy.ts (1)
1-1: Server-only guard and API-boundary required; this module must not be imported by client code.This file is imported by a client component (components/map/mapbox-map.tsx Line 14). Secrets will be undefined in the browser and risk leakage if ever bundled. Make this module server-only and consume it via an API route.
Apply this diff:
+import 'server-only';Follow-up: expose a server API (e.g., app/api/webcams/route.ts) that reads WINDY_API_KEY and proxies Windy requests; have the client fetch that route instead of importing this file.
components/map/mapbox-map.tsx (4)
14-14: Do not import server-only Windy client into a client component. Switch to your internal API route.Replace the import; define a local lightweight type for fetched data.
Apply this diff:
-import { getWebcams, Webcam } from '@/lib/windy'; +// Fetch from server API route; keep types local to avoid importing server code +type Webcam = { + id: string; + title?: string; + location: { latitude: number; longitude: number }; + images?: { current?: { preview?: string } }; + urls?: { webcam?: string }; +};
512-530: Fetch via API route, bound to viewport, and abort on mode change/unmount.Avoid secret usage, reduce payload, and prevent setState after unmount.
Apply this diff:
- useEffect(() => { - if (mapType === MapToggleEnum.WebcamMode) { - const fetchWebcams = async () => { - try { - const windyResponse = await getWebcams(); - setWebcams(windyResponse.webcams); - } catch (error) { - console.error('Error fetching webcams:', error); - toast.error('Failed to fetch webcams.'); - } - }; - fetchWebcams(); - } else { - // Clear webcams and markers when not in WebcamMode - setWebcams([]); - Object.values(webcamMarkersRef.current).forEach(marker => marker.remove()); - webcamMarkersRef.current = {}; - } - }, [mapType]); + useEffect(() => { + const ac = new AbortController(); + const run = async () => { + if (mapType !== MapToggleEnum.WebcamMode) { + setWebcams([]); + Object.values(webcamMarkersRef.current).forEach(m => m.remove()); + webcamMarkersRef.current = {}; + return; + } + try { + const b = map.current?.getBounds(); + const bbox = b ? `${b.getWest()},${b.getSouth()},${b.getEast()},${b.getNorth()}` : undefined; + const url = new URL('/api/webcams', window.location.origin); + if (bbox) url.searchParams.set('bbox', bbox); + const res = await fetch(url.toString(), { signal: ac.signal, cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = (await res.json()) as { webcams?: Webcam[] }; + setWebcams(data.webcams ?? []); + } catch (err: any) { + if (err.name !== 'AbortError') { + console.error('Error fetching webcams:', err); + toast.error('Failed to fetch webcams.'); + } + } + }; + run(); + return () => ac.abort(); + }, [mapType]);Optional: re-fetch on map moveend while in WebcamMode (throttled/debounced). I can provide that snippet if desired.
532-559: Avoid XSS: replace setHTML with setDOMContent; also guard missing fields.Do not interpolate untrusted text/URLs into HTML.
Apply this diff:
- webcams.forEach(webcam => { + webcams.forEach(webcam => { const el = document.createElement('div'); el.className = 'webcam-marker'; - el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/web-camera.png")'; + // Prefer local asset or inline SVG; see separate comment. el.style.width = '25px'; el.style.height = '25px'; el.style.backgroundSize = 'cover'; el.style.borderRadius = '50%'; el.style.cursor = 'pointer'; - const marker = new mapboxgl.Marker(el) - .setLngLat([webcam.location.longitude, webcam.location.latitude]) - .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML( - `<a href="${webcam.urls.webcam}" target="_blank" rel="noopener noreferrer"><h3>${webcam.title}</h3><img src="${webcam.images.current.preview}" alt="${webcam.title}" style="width:100%;" /></a>` - )) - .addTo(map.current!); + const popupRoot = document.createElement('div'); + const h3 = document.createElement('h3'); + h3.textContent = webcam.title ?? 'Webcam'; + popupRoot.appendChild(h3); + const preview = webcam.images?.current?.preview; + if (preview) { + const img = document.createElement('img'); + img.src = preview; + img.alt = webcam.title ?? 'Webcam preview'; + img.style.width = '100%'; + popupRoot.appendChild(img); + } + const link = document.createElement('a'); + link.textContent = 'Open webcam'; + link.href = webcam.urls?.webcam ?? '#'; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + popupRoot.appendChild(link); + + const marker = new mapboxgl.Marker(el) + .setLngLat([webcam.location.longitude, webcam.location.latitude]) + .setPopup(new mapboxgl.Popup({ offset: 25 }).setDOMContent(popupRoot)) + .addTo(map.current!); webcamMarkersRef.current[webcam.id] = marker; });Add unmount cleanup where you tear down the map: iterate webcamMarkersRef.current, remove all, then reset the ref, to avoid DOM leaks when unmounting during WebcamMode.
541-547: External icon URL is brittle; use local asset or inline SVG.Prevents CSP/mixed-content/3rd‑party dependency issues.
Example:
- el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/web-camera.png")'; + el.innerHTML = '<svg viewBox="0 0 24 24" width="24" height="24" fill="#111"><path d="M4 6h12a2 2 0 0 1 2 2v1l3-2v10l-3-2v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/></svg>';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
components/map/mapbox-map.tsx(4 hunks)lib/windy.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
components/map/mapbox-map.tsx (1)
lib/windy.ts (2)
Webcam(1-19)getWebcams(25-43)
🔇 Additional comments (2)
components/map/mapbox-map.tsx (1)
36-38: LGTM on marker bookkeeping.Tracking markers in a ref keyed by id is appropriate for cleanup.
lib/windy.ts (1)
25-36: Verified: Windy API v3 base URL and auth header are correct.Base URL https://api.windy.com/webcams/api/v3/webcams and auth header X-WINDY-API-KEY match the docs; the code's 'x-windy-api-key' is acceptable (HTTP headers are case‑insensitive). Use include=images,urls to request images/URLs and bbox={north_lat},{east_lon},{south_lat},{west_lon} for bbox filtering.
| useEffect(() => { | ||
| if (mapType === MapToggleEnum.WebcamMode) { | ||
| const fetchWebcams = async () => { | ||
| try { | ||
| const windyResponse = await getWebcams(); | ||
| setWebcams(windyResponse.webcams); | ||
| } catch (error) { | ||
| console.error('Error fetching webcams:', error); | ||
| toast.error('Failed to fetch webcams.'); | ||
| } | ||
| }; | ||
| fetchWebcams(); | ||
| } else { | ||
| // Clear webcams and markers when not in WebcamMode | ||
| setWebcams([]); | ||
| Object.values(webcamMarkersRef.current).forEach(marker => marker.remove()); | ||
| webcamMarkersRef.current = {}; | ||
| } | ||
| }, [mapType]); | ||
|
|
||
| useEffect(() => { | ||
| if (map.current && mapType === MapToggleEnum.WebcamMode) { | ||
| // Clear existing markers | ||
| Object.values(webcamMarkersRef.current).forEach(marker => marker.remove()); | ||
| webcamMarkersRef.current = {}; | ||
|
|
||
| webcams.forEach(webcam => { | ||
| const el = document.createElement('div'); | ||
| el.className = 'webcam-marker'; | ||
| el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/web-camera.png")'; | ||
| el.style.width = '25px'; | ||
| el.style.height = '25px'; | ||
| el.style.backgroundSize = 'cover'; | ||
| el.style.borderRadius = '50%'; | ||
| el.style.cursor = 'pointer'; | ||
|
|
||
| const marker = new mapboxgl.Marker(el) | ||
| .setLngLat([webcam.location.longitude, webcam.location.latitude]) | ||
| .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML( | ||
| `<a href="${webcam.urls.webcam}" target="_blank" rel="noopener noreferrer"><h3>${webcam.title}</h3><img src="${webcam.images.current.preview}" alt="${webcam.title}" style="width:100%;" /></a>` | ||
| )) | ||
| .addTo(map.current!); | ||
|
|
||
| webcamMarkersRef.current[webcam.id] = marker; | ||
| }); | ||
| } | ||
| }, [webcams, mapType]); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Operational: limit payloads and add client/server-side rate limiting.
Query by current bounds, cap results (limit/page), and add rudimentary rate limiting on the API route to avoid exhausting 3rd‑party quotas when panning frequently.
lib/windy.ts
Outdated
| export interface Webcam { | ||
| id: string; | ||
| title: string; | ||
| location: { | ||
| latitude: number; | ||
| longitude: number; | ||
| }; | ||
| images: { | ||
| current: { | ||
| preview: string; | ||
| }; | ||
| daylight: { | ||
| preview: string; | ||
| } | ||
| }; | ||
| urls: { | ||
| webcam: string; | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Make optional fields optional to match real-world API variability.
Downstream code assumes images/urls always exist; loosen the type to prevent runtime crashes.
Apply this diff:
export interface Webcam {
id: string;
title: string;
location: {
latitude: number;
longitude: number;
};
- images: {
- current: {
- preview: string;
- };
- daylight: {
- preview: string;
- }
- };
- urls: {
- webcam: string;
- };
+ images?: {
+ current?: { preview?: string };
+ daylight?: { preview?: string };
+ };
+ urls?: { webcam?: string };
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export interface Webcam { | |
| id: string; | |
| title: string; | |
| location: { | |
| latitude: number; | |
| longitude: number; | |
| }; | |
| images: { | |
| current: { | |
| preview: string; | |
| }; | |
| daylight: { | |
| preview: string; | |
| } | |
| }; | |
| urls: { | |
| webcam: string; | |
| }; | |
| } | |
| export interface Webcam { | |
| id: string; | |
| title: string; | |
| location: { | |
| latitude: number; | |
| longitude: number; | |
| }; | |
| images?: { | |
| current?: { | |
| preview?: string; | |
| }; | |
| daylight?: { | |
| preview?: string; | |
| }; | |
| }; | |
| urls?: { | |
| webcam?: string; | |
| }; | |
| } |
🤖 Prompt for AI Agents
In lib/windy.ts around lines 1 to 19, the Webcam interface currently requires
images and urls but the real API may omit them; update the type to make images
and urls optional and make nested properties that may be missing optional as
well (e.g., images?, images.current?, images.daylight?, their preview strings?,
and urls?). Adjust the interface so consumers must handle undefined (use
optional properties or unions with undefined) to prevent runtime crashes when
fields are absent.
lib/windy.ts
Outdated
| export async function getWebcams(): Promise<WindyResponse> { | ||
| const apiKey = process.env.WINDY_API_KEY; | ||
| if (!apiKey) { | ||
| throw new Error('WINDY_API_KEY is not defined in the environment variables.'); | ||
| } | ||
|
|
||
| const response = await fetch('https://api.windy.com/webcams/api/v3/webcams', { | ||
| method: 'GET', | ||
| headers: { | ||
| 'x-windy-api-key': apiKey, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Consider parameterizing by viewport (bbox) and limit on the server.
Fetch-all is heavy; accept query params (e.g., bbox) and forward to Windy; do this in the API route to keep lib/windy server-only.
Happy to draft the route handler if you want.
lib/windy.ts
Outdated
| const response = await fetch('https://api.windy.com/webcams/api/v3/webcams', { | ||
| method: 'GET', | ||
| headers: { | ||
| 'x-windy-api-key': apiKey, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Disable caching and declare Accept header for external fetch.
Avoid unintended ISR caching and ensure JSON negotiation.
Apply this diff:
const response = await fetch('https://api.windy.com/webcams/api/v3/webcams', {
method: 'GET',
headers: {
'x-windy-api-key': apiKey,
+ 'accept': 'application/json',
},
+ cache: 'no-store',
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const response = await fetch('https://api.windy.com/webcams/api/v3/webcams', { | |
| method: 'GET', | |
| headers: { | |
| 'x-windy-api-key': apiKey, | |
| }, | |
| }); | |
| const response = await fetch('https://api.windy.com/webcams/api/v3/webcams', { | |
| method: 'GET', | |
| headers: { | |
| 'x-windy-api-key': apiKey, | |
| 'accept': 'application/json', | |
| }, | |
| cache: 'no-store', | |
| }); |
🤖 Prompt for AI Agents
In lib/windy.ts around lines 31 to 36, the external fetch call needs explicit
JSON negotiation and caching disabled: add an 'Accept': 'application/json'
header and also set fetch to avoid cached responses (e.g., include cache:
'no-store' in the fetch options and/or add 'Cache-Control': 'no-cache' header)
so the request always returns fresh JSON; update the fetch options accordingly.
lib/windy.ts
Outdated
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch webcams: ${response.statusText}`); | ||
| } | ||
|
|
||
| return response.json(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Improve errors with status code and body for observability.
Current error hides details; include code and response body.
Apply this diff:
- if (!response.ok) {
- throw new Error(`Failed to fetch webcams: ${response.statusText}`);
- }
-
- return response.json();
+ if (!response.ok) {
+ const body = await response.text().catch(() => '');
+ throw new Error(`Windy webcams fetch failed: ${response.status} ${response.statusText}${body ? ` — ${body.slice(0, 500)}` : ''}`);
+ }
+ return (await response.json()) as WindyResponse;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!response.ok) { | |
| throw new Error(`Failed to fetch webcams: ${response.statusText}`); | |
| } | |
| return response.json(); | |
| } | |
| if (!response.ok) { | |
| const body = await response.text().catch(() => ''); | |
| throw new Error(`Windy webcams fetch failed: ${response.status} ${response.statusText}${body ? ` — ${body.slice(0, 500)}` : ''}`); | |
| } | |
| return (await response.json()) as WindyResponse; | |
| } |
🤖 Prompt for AI Agents
lib/windy.ts around lines 38 to 43: the current error thrown on non-OK responses
only includes response.statusText — replace this with code that reads the
response body (await response.text() or attempt JSON parse), then throw an Error
whose message includes response.status (and statusText) plus the response body
content to improve observability; ensure you await the body read before throwing
so the error contains the actual response payload.
This commit renames all occurrences of "webcam" to "sensor" in the code, as requested by the user. This includes variable names, function names, interface names, and strings. The following files were modified: - `lib/sensors.ts` (renamed from `lib/windy.ts`) - `components/map-toggle-context.tsx` - `components/map-toggle.tsx` - `components/map/mapbox-map.tsx`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/map-toggle-context.tsx (1)
39-41: Fix typos in error message.Improve clarity and professionalism.
- if (context === undefined) { - throw new Error('map toogle context must be used within an map toggle provider'); - } + if (context === undefined) { + throw new Error('MapToggleContext must be used within a MapToggleProvider'); + }
♻️ Duplicate comments (6)
components/map-toggle.tsx (1)
11-11: Do not shadow the global Map constructor. Rename the icon import.-import { Map, Pencil, RadioTower } from 'lucide-react' +import { Map as MapIcon, Pencil, RadioTower } from 'lucide-react'And update usage:
- <Map className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> + <MapIcon className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />components/map/mapbox-map.tsx (5)
14-14: Do not import server-only fetcher into a client component. Fetch via API route instead.-import { getSensors, Sensor } from '@/lib/sensors'; +// Do not import server-only code here. Define a local type and call /api/sensors. +type Sensor = { + id: string; + title?: string; + location: { latitude: number; longitude: number }; + images?: { current?: { preview?: string } }; + urls?: { webcam?: string }; +};
512-530: Fetch sensors via API route, scoped to viewport, and abort on mode switch.- useEffect(() => { - if (mapType === MapToggleEnum.SensorMode) { - const fetchSensors = async () => { - try { - const windyResponse = await getSensors(); - setSensors(windyResponse.webcams); - } catch (error) { - console.error('Error fetching sensors:', error); - toast.error('Failed to fetch sensors.'); - } - }; - fetchSensors(); - } else { - // Clear sensors and markers when not in SensorMode - setSensors([]); - Object.values(sensorMarkersRef.current).forEach(marker => marker.remove()); - sensorMarkersRef.current = {}; - } - }, [mapType]); + useEffect(() => { + const ac = new AbortController(); + const run = async () => { + if (mapType !== MapToggleEnum.SensorMode) { + setSensors([]); + Object.values(sensorMarkersRef.current).forEach(m => m.remove()); + sensorMarkersRef.current = {}; + return; + } + try { + const bbox = map.current?.getBounds(); + const bboxStr = bbox + ? `${bbox.getWest()},${bbox.getSouth()},${bbox.getEast()},${bbox.getNorth()}` + : undefined; + const url = new URL('/api/sensors', window.location.origin); + if (bboxStr) url.searchParams.set('bbox', bboxStr); + const res = await fetch(url.toString(), { signal: ac.signal, cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = (await res.json()) as { webcams?: Sensor[]; sensors?: Sensor[] }; + setSensors(data.webcams ?? data.sensors ?? []); + } catch (err: any) { + if (err.name !== 'AbortError') { + console.error('Error fetching sensors:', err); + toast.error('Failed to fetch sensors.'); + } + } + }; + run(); + return () => ac.abort(); + }, [mapType]);Also add unmount cleanup to avoid leaks (outside this hunk):
Object.values(lineLabelsRef.current).forEach(marker => marker.remove()) + Object.values(sensorMarkersRef.current).forEach(m => m.remove()) + sensorMarkersRef.current = {}
539-547: Avoid external icon URL for markers; use inline SVG or a local asset.- el.className = 'sensor-marker'; - el.style.backgroundImage = 'url("https://img.icons8.com/ios-filled/50/000000/radio-tower.png")'; - el.style.width = '25px'; - el.style.height = '25px'; - el.style.backgroundSize = 'cover'; - el.style.borderRadius = '50%'; - el.style.cursor = 'pointer'; + el.className = 'sensor-marker'; + el.style.width = '25px'; + el.style.height = '25px'; + el.style.display = 'grid'; + el.style.placeItems = 'center'; + el.style.borderRadius = '50%'; + el.style.cursor = 'pointer'; + el.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0f172a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v8"/><path d="M5 10a7 7 0 0 1 14 0"/><path d="M8 10a4 4 0 0 1 8 0"/><circle cx="12" cy="18" r="2"/><path d="M12 20v2"/></svg>`;
548-556: XSS: do not use setHTML with untrusted data. Build DOM and use setDOMContent.- const marker = new mapboxgl.Marker(el) - .setLngLat([sensor.location.longitude, sensor.location.latitude]) - .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML( - `<a href="${sensor.urls.webcam}" target="_blank" rel="noopener noreferrer"><h3>${sensor.title}</h3><img src="${sensor.images.current.preview}" alt="${sensor.title}" style="width:100%;" /></a>` - )) - .addTo(map.current!); + const marker = new mapboxgl.Marker(el) + .setLngLat([sensor.location.longitude, sensor.location.latitude]); + + const popupRoot = document.createElement('div'); + const a = document.createElement('a'); + const h3 = document.createElement('h3'); + const img = document.createElement('img'); + + h3.textContent = sensor.title ?? 'Webcam'; + const href = sensor.urls?.webcam; + if (href && /^https?:\/\//i.test(href)) { + a.href = href; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + } + const preview = sensor.images?.current?.preview; + if (preview && /^https?:\/\//i.test(preview)) { + img.src = preview; + img.alt = sensor.title ?? 'Webcam preview'; + img.style.width = '100%'; + a.append(h3, img); + } else { + a.append(h3); + } + popupRoot.append(a); + + marker.setPopup(new mapboxgl.Popup({ offset: 25 }).setDOMContent(popupRoot)) + .addTo(map.current!);
532-559: Guard against missing fields and duplicate IDs.Skip invalid points and avoid crashes if images/urls are missing; ensure unique keys.
- sensors.forEach(sensor => { + sensors.forEach((sensor, idx) => { + if (!sensor?.location || typeof sensor.location.longitude !== 'number' || typeof sensor.location.latitude !== 'number') return; + const key = sensor.id ?? `sensor-${idx}-${sensor.location.longitude}-${sensor.location.latitude}`;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
components/map-toggle-context.tsx(1 hunks)components/map-toggle.tsx(2 hunks)components/map/mapbox-map.tsx(4 hunks)lib/sensors.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
components/map/mapbox-map.tsx (1)
lib/sensors.ts (2)
Sensor(1-19)getSensors(25-56)
🪛 Biome (2.1.2)
components/map-toggle.tsx
[error] 11-11: Do not shadow the global "Map" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
| SensorMode, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Prefer string enum to avoid ordinal coupling; consider aligning name with “Webcams”.
Use string values to prevent future reordering bugs; also consider renaming SensorMode to WebcamMode for consistency with the feature and PR wording.
Apply:
-export enum MapToggleEnum {
- FreeMode,
- RealTimeMode,
- DrawingMode, // Added new mode for drawing and measurement
- SensorMode,
-}
+export enum MapToggleEnum {
+ FreeMode = 'FreeMode',
+ RealTimeMode = 'RealTimeMode',
+ DrawingMode = 'DrawingMode', // Added new mode for drawing and measurement
+ SensorMode = 'SensorMode', // consider: 'WebcamMode'
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| SensorMode, | |
| } | |
| export enum MapToggleEnum { | |
| FreeMode = 'FreeMode', | |
| RealTimeMode = 'RealTimeMode', | |
| DrawingMode = 'DrawingMode', // Added new mode for drawing and measurement | |
| SensorMode = 'SensorMode', // consider: 'WebcamMode' | |
| } |
🤖 Prompt for AI Agents
In components/map-toggle-context.tsx around lines 9-10, the enum currently uses
numeric/ordinal values and is named SensorMode; replace it with a string enum
named WebcamMode (e.g., export enum WebcamMode { On = "On", Off = "Off", ... })
to avoid ordinal coupling and align naming with the feature. Update all
imports/usages, type annotations, switch/case and default handling, plus any
serialization/deserialization, localStorage or API payloads to use the string
values and the new name, and update tests and snapshots accordingly.
components/map-toggle.tsx
Outdated
| <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}> | ||
| <RadioTower className="h-[1rem] w-[1rem] mr-2" /> | ||
| Sensors | ||
| </DropdownMenuItem> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Align UI label with the feature (“Webcams” vs “Sensors”).
PR and API integrate Windy Webcams; label it “Webcams” to avoid confusion, or rename the enum to WebcamMode for full consistency.
- Sensors
+ Webcams📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}> | |
| <RadioTower className="h-[1rem] w-[1rem] mr-2" /> | |
| Sensors | |
| </DropdownMenuItem> | |
| <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}> | |
| <RadioTower className="h-[1rem] w-[1rem] mr-2" /> | |
| Webcams | |
| </DropdownMenuItem> |
🤖 Prompt for AI Agents
In components/map-toggle.tsx around lines 32 to 35, the dropdown item label
reads "Sensors" but the feature is Windy Webcams; update the UI to say "Webcams"
to match the integrated feature. Change the displayed text to "Webcams" and also
consider renaming the enum value (e.g., MapToggleEnum.SensorMode →
MapToggleEnum.WebcamMode) and update all references across the codebase (props,
state, switches, tests, and translations) to maintain consistency.
| export interface Sensor { | ||
| id: string; | ||
| title: string; | ||
| location: { | ||
| latitude: number; | ||
| longitude: number; | ||
| }; | ||
| images: { | ||
| current: { | ||
| preview: string; | ||
| }; | ||
| daylight: { | ||
| preview: string; | ||
| } | ||
| }; | ||
| urls: { | ||
| webcam: string; | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Naming: “Sensor” may confuse; prefer “Webcam” types.
Aligning names with the data source improves clarity across UI and API.
-export interface Sensor {
+export interface Webcam {
id: string;
title: string;
location: {
latitude: number;
longitude: number;
};
images: {
current: {
preview: string;
};
daylight: {
preview: string;
}
};
urls: {
webcam: string;
};
}
-export interface WindyResponse {
- webcams: Sensor[];
+export interface WindyResponse {
+ webcams: Webcam[];
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In lib/sensors.ts lines 1-19, the exported interface is named Sensor but should
be renamed to Webcam to match the data source; change the interface declaration
and its export from Sensor to Webcam, update any JSDoc or inline comments
accordingly, and then update all project references/imports to use the new
Webcam type (search & replace imports and type annotations) to keep API and UI
naming consistent.
| export interface WindyResponse { | ||
| webcams: Sensor[]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Confirm response shape matches Windy API.
Many APIs nest results; add runtime validation (e.g., zod) or adapt to the actual schema before returning.
🌐 Web query:
Windy Webcams API v3: What is the exact JSON shape for GET /webcams? Does it return { webcams: [...] } or a nested object?
💡 Result:
Short answer: GET /webcams (Webcams API v3) returns a top-level JSON array of webcam objects (WebcamV3Schema[]), not an object like { "webcams": [...] }. See the V3 docs/transfer notes. [1][2]
Sources:
- Windy Webcams API docs (V3 overview). [1]
- Migration / result schema notes showing V3 returns WebcamV3Schema[] (V2 used { result: { webcams: [...] } }). [2]
Windy v3 returns a top-level array — update types & add runtime validation
Windy Webcams API v3 returns a top-level array of webcam objects (WebcamV3Schema[]). lib/sensors.ts defines WindyResponse as { webcams: Sensor[] } which is incorrect.
- Fix: replace/adapt to the actual shape (e.g., export type WindyResponse = Sensor[] or change the fetch/parsing to map the returned array into { webcams }).
- Add runtime validation (zod or similar) to assert the array and element schema before use.
Location: lib/sensors.ts (lines 21–23)
| export async function getSensors(): Promise<WindyResponse> { | ||
| const apiKey = process.env.WINDY_API_KEY; | ||
| if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') { | ||
| console.error('WINDY_API_KEY is not defined or not set in the environment variables.'); | ||
| throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.'); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Secret usage in a client-importable module.
This module is imported by a client component (Mapbox). Accessing process.env.WINDY_API_KEY here will either leak the key (if bundled) or be undefined at runtime. Fetch secrets server-side only and expose a safe API route.
Apply server-only guard and move fetch to an API route; then have clients call the route:
+import 'server-only';Example API route (add new file app/api/sensors/route.ts):
// app/api/sensors/route.ts
import 'server-only';
export const dynamic = 'force-dynamic';
export async function GET(req: Request) {
const apiKey = process.env.WINDY_API_KEY;
if (!apiKey) {
return new Response(JSON.stringify({ error: 'Missing WINDY_API_KEY' }), { status: 500 });
}
const { searchParams } = new URL(req.url);
const bbox = searchParams.get('bbox') ?? '';
const url = new URL('https://api.windy.com/webcams/api/v3/webcams');
url.searchParams.set('include', 'location,images,urls');
if (bbox) url.searchParams.set('bbox', bbox);
const ac = new AbortController();
const to = setTimeout(() => ac.abort(), 10000);
try {
const res = await fetch(url.toString(), {
headers: { 'x-windy-api-key': apiKey },
signal: ac.signal,
cache: 'no-store',
});
if (!res.ok) {
const txt = await res.text();
return new Response(JSON.stringify({ error: txt || res.statusText }), { status: res.status });
}
const json = await res.json();
return new Response(JSON.stringify(json), { status: 200 });
} catch (e) {
const msg = (e as Error)?.message ?? 'unknown error';
return new Response(JSON.stringify({ error: msg }), { status: 500 });
} finally {
clearTimeout(to);
}
}Then update the client to fetch /api/sensors (see mapbox-map.tsx comments).
| if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') { | ||
| console.error('WINDY_API_KEY is not defined or not set in the environment variables.'); | ||
| throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Unify placeholder value with .env samples.
Use the same placeholder everywhere to reduce confusion.
- if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') {
+ if (!apiKey || apiKey === 'your_windy_api_key_here') {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') { | |
| console.error('WINDY_API_KEY is not defined or not set in the environment variables.'); | |
| throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.'); | |
| } | |
| if (!apiKey || apiKey === 'your_windy_api_key_here') { | |
| console.error('WINDY_API_KEY is not defined or not set in the environment variables.'); | |
| throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.'); | |
| } |
🤖 Prompt for AI Agents
In lib/sensors.ts around lines 27 to 30, the code compares apiKey to the literal
'YOUR_WINDY_API_KEY' which differs from the placeholder used in our .env
samples; change that literal to exactly match the placeholder string used in the
repository's .env example (replace 'YOUR_WINDY_API_KEY' with the .env sample
placeholder) so the environment-check is consistent across code and docs, and
keep the error message text aligned with that same placeholder.
| console.log('Fetching sensors with API key:', apiKey ? 'present' : 'missing'); | ||
| const response = await fetch('https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls', { | ||
| method: 'GET', | ||
| headers: { | ||
| 'x-windy-api-key': apiKey, | ||
| }, | ||
| }); | ||
|
|
||
| console.log('Windy API response status:', response.status); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add timeout/abort and no-store caching to the upstream fetch.
Prevents hung requests and stale data; also safer for quota.
- const response = await fetch('https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls', {
- method: 'GET',
- headers: {
- 'x-windy-api-key': apiKey,
- },
- });
+ const ac = new AbortController();
+ const timeout = setTimeout(() => ac.abort(), 10000);
+ const response = await fetch(
+ 'https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls',
+ {
+ method: 'GET',
+ headers: { 'x-windy-api-key': apiKey },
+ signal: ac.signal,
+ cache: 'no-store',
+ }
+ );
+ clearTimeout(timeout);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log('Fetching sensors with API key:', apiKey ? 'present' : 'missing'); | |
| const response = await fetch('https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls', { | |
| method: 'GET', | |
| headers: { | |
| 'x-windy-api-key': apiKey, | |
| }, | |
| }); | |
| console.log('Windy API response status:', response.status); | |
| console.log('Fetching sensors with API key:', apiKey ? 'present' : 'missing'); | |
| const ac = new AbortController(); | |
| const timeout = setTimeout(() => ac.abort(), 10000); | |
| const response = await fetch( | |
| 'https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls', | |
| { | |
| method: 'GET', | |
| headers: { 'x-windy-api-key': apiKey }, | |
| signal: ac.signal, | |
| cache: 'no-store', | |
| } | |
| ); | |
| clearTimeout(timeout); | |
| console.log('Windy API response status:', response.status); |
🤖 Prompt for AI Agents
In lib/sensors.ts around lines 33 to 41, the upstream fetch lacks a
timeout/abort mechanism and cache control; wrap the request in an
AbortController with a setTimeout that calls controller.abort() after a
reasonable ms (e.g. 5-10s), pass controller.signal to fetch, clear the timeout
after fetch completes, and handle the abort exception pathway; also add fetch
option cache: 'no-store' to prevent caching of responses and ensure fresh
requests and safer quota usage.
| const data = await response.json(); | ||
| console.log('Successfully fetched sensor data:', data); | ||
| return data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Avoid logging entire payloads.
Large logs hurt perf and may expose data unnecessarily.
- console.log('Successfully fetched sensor data:', data);
+ console.debug('Successfully fetched sensor data');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const data = await response.json(); | |
| console.log('Successfully fetched sensor data:', data); | |
| return data; | |
| const data = await response.json(); | |
| console.debug('Successfully fetched sensor data'); | |
| return data; |
🤖 Prompt for AI Agents
In lib/sensors.ts around lines 49 to 51, the code currently logs the entire
fetched payload (console.log('Successfully fetched sensor data:', data)); remove
or replace that full-payload log to avoid performance and data-exposure issues —
either drop the log entirely or log a small summary (e.g., number of records,
keys, or first N items) and/or use a conditional debug logger tied to an
environment flag; if sensitive fields exist, redact them before logging. Ensure
you do not stringify or print the whole `data` object in production code.
This commit implements the "Sensors" mode, which displays sensor data from the Windy.com API on the map. The following changes were made: - Created a new `SensorToggle` component triggered by a `TentTree` icon, which contains the "Sensors" map mode option. - Created a new `MapToggle` component for the other map modes, triggered by a `Map` icon. - Updated the marker style for sensors to be a circle with an arrow, as requested. - Restored the header and mobile navigation bar to their original icon order. - Added extensive logging to the `getSensors` function to help with debugging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
components/header.tsx(2 hunks)components/map-toggle.tsx(2 hunks)components/map/mapbox-map.tsx(4 hunks)components/mobile-icons-bar.tsx(2 hunks)components/sensor-toggle.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
components/sensor-toggle.tsx (1)
components/map-toggle-context.tsx (6)
useMapToggle(37-43)mapToggleState(22-34)MapToggleContextType(11-14)MapToggleEnum(5-9)type(25-27)MapToggleProviderProps(18-20)
components/map/mapbox-map.tsx (2)
lib/sensors.ts (2)
Sensor(1-19)getSensors(25-56)components/map/map-data-context.tsx (1)
MapData(7-17)
components/header.tsx (1)
components/sensor-toggle.tsx (1)
SensorToggle(14-32)
components/mobile-icons-bar.tsx (1)
components/sensor-toggle.tsx (1)
SensorToggle(14-32)
🔇 Additional comments (6)
components/header.tsx (1)
46-46: SensorToggle integration looks good.Header wiring to SensorToggle is clear and aligns with the new mode.
components/mobile-icons-bar.tsx (1)
18-19: Mobile toggles integration looks good.Using MapToggle + SensorToggle here is consistent with header.
components/map-toggle.tsx (1)
20-21: Alias Map icon to avoid shadowing and add an accessible name.Rename the lucide
Mapimport toMapIconand update usage. Also add an aria-label to the icon-only button for a11y. Prior feedback noted a lint error here.- <Button variant="ghost" size="icon"> - <Map className="h-[1.2rem] w-[1.2rem]" /> + <Button variant="ghost" size="icon" aria-label="Map modes"> + <MapIcon className="h-[1.2rem] w-[1.2rem]" />Additionally update the import (outside this hunk):
import { Map as MapIcon, Pencil } from 'lucide-react'Also, per PR objectives, confirm whether the “Webcams” option is intentionally not in this menu (moved to SensorToggle). If it should be here, add a dropdown item wired to Webcam/Sensor mode.
components/map/mapbox-map.tsx (3)
14-14: Security: Don’t import server-only client (getSensors) in a client component.This can expose WINDY_API_KEY via bundling or misuse. Fetch via a server API route instead (e.g., /api/webcams or /api/sensors).
-import { getSensors, Sensor } from '@/lib/sensors'; +// Import only types (or re-declare locally) and fetch via API route. +import type { Sensor } from '@/lib/sensors';Then update the fetching effect (see below) to call your API route with AbortController and optional bbox.
552-556: XSS risk: setHTML with untrusted values. Use setDOMContent with created nodes.Titles/URLs/images come from a 3rd-party API; building a DOM fragment avoids HTML injection.
- const marker = new mapboxgl.Marker(el) - .setLngLat([sensor.location.longitude, sensor.location.latitude]) - .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML( - `<a href="${sensor.urls.webcam}" target="_blank" rel="noopener noreferrer"><h3>${sensor.title}</h3><img src="${sensor.images.current.preview}" alt="${sensor.title}" style="width:100%;" /></a>` - )) - .addTo(map.current!); + const popupRoot = document.createElement('div'); + const a = document.createElement('a'); + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + // Optionally validate/whitelist URL protocol/domain before assigning: + a.href = sensor.urls?.webcam ?? '#'; + const h3 = document.createElement('h3'); + h3.textContent = sensor.title ?? 'Webcam'; + a.appendChild(h3); + const preview = sensor.images?.current?.preview; + if (preview) { + const img = document.createElement('img'); + img.src = preview; + img.alt = sensor.title ?? 'Webcam preview'; + img.style.width = '100%'; + a.appendChild(img); + } + popupRoot.appendChild(a); + + const marker = new mapboxgl.Marker(el) + .setLngLat([sensor.location.longitude, sensor.location.latitude]) + .setPopup(new mapboxgl.Popup({ offset: 25 }).setDOMContent(popupRoot)) + .addTo(map.current!);
512-530: Fetch via internal API route, support viewport bbox, and abort on mode switch/unmount.Avoid using the secret on the client; reduce payloads and race conditions.
- useEffect(() => { - if (mapType === MapToggleEnum.SensorMode) { - const fetchSensors = async () => { - try { - const windyResponse = await getSensors(); - setSensors(windyResponse.webcams); - } catch (error) { - console.error('Error fetching sensors:', error); - toast.error('Failed to fetch sensors.'); - } - }; - fetchSensors(); - } else { - // Clear sensors and markers when not in SensorMode - setSensors([]); - Object.values(sensorMarkersRef.current).forEach(marker => marker.remove()); - sensorMarkersRef.current = {}; - } - }, [mapType]); + useEffect(() => { + const ac = new AbortController(); + const run = async () => { + if (mapType !== MapToggleEnum.SensorMode) { + setSensors([]); + Object.values(sensorMarkersRef.current).forEach(m => m.remove()); + sensorMarkersRef.current = {}; + return; + } + try { + const bbox = map.current?.getBounds(); + const bboxStr = bbox + ? `${bbox.getWest()},${bbox.getSouth()},${bbox.getEast()},${bbox.getNorth()}` + : undefined; + const url = new URL('/api/webcams', window.location.origin); + if (bboxStr) url.searchParams.set('bbox', bboxStr); + const res = await fetch(url.toString(), { signal: ac.signal, cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = (await res.json()) as { webcams?: Sensor[] }; + setSensors(data.webcams ?? []); + } catch (err) { + if ((err as any).name !== 'AbortError') { + console.error('Error fetching sensors:', err); + toast.error('Failed to fetch webcams.'); + } + } + }; + run(); + return () => { + ac.abort(); + // Ensure markers are cleaned up on unmount + Object.values(sensorMarkersRef.current).forEach(m => m.remove()); + sensorMarkersRef.current = {}; + }; + }, [mapType]);Operational follow-up:
- Add simple server-side rate limiting/caching to the API route to protect Windy quotas.
- Consider pagination/limit in the API (and client) to cap marker counts.
| TentTree, | ||
| } from 'lucide-react' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Remove unused TentTree import.
It’s no longer referenced after introducing SensorToggle; drop it to avoid lints.
-import {
- Search,
- CircleUserRound,
- CalendarDays,
- TentTree,
-} from 'lucide-react'
+import {
+ Search,
+ CircleUserRound,
+ CalendarDays,
+} from 'lucide-react'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| TentTree, | |
| } from 'lucide-react' | |
| import { | |
| Search, | |
| CircleUserRound, | |
| CalendarDays, | |
| } from 'lucide-react' |
🤖 Prompt for AI Agents
In components/header.tsx around lines 11 to 12, the TentTree import from
'lucide-react' is unused after introducing SensorToggle; remove TentTree from
the import list so only referenced icons remain to satisfy linters and keep
imports minimal.
| </DropdownMenuItem> | ||
| <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.DrawingMode)}}> | ||
| <Pencil className="h-[1rem] w-[1rem] mr-2" /> | ||
| <Pencil className="h-[1rem] w-[1.2rem] mr-2" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Normalize icon sizing (nit).
For visual consistency with other icons, consider matching height to 1.2rem.
- <Pencil className="h-[1rem] w-[1.2rem] mr-2" />
+ <Pencil className="h-[1.2rem] w-[1.2rem] mr-2" />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Pencil className="h-[1rem] w-[1.2rem] mr-2" /> | |
| <Pencil className="h-[1.2rem] w-[1.2rem] mr-2" /> |
🤖 Prompt for AI Agents
In components/map-toggle.tsx around line 32, the Pencil icon uses className
"h-[1rem] w-[1.2rem] mr-2" which makes its height inconsistent with other icons;
change the height to 1.2rem to match (e.g., update className to use h-[1.2rem]
w-[1.2rem] mr-2) so the icon dimensions are normalized with the rest of the UI.
| sensors.forEach(sensor => { | ||
| const el = document.createElement('div'); | ||
| el.className = 'sensor-marker'; | ||
| el.style.width = '30px'; | ||
| el.style.height = '30px'; | ||
| el.style.borderRadius = '50%'; | ||
| el.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; | ||
| el.style.color = 'white'; | ||
| el.style.display = 'flex'; | ||
| el.style.justifyContent = 'center'; | ||
| el.style.alignItems = 'center'; | ||
| el.style.cursor = 'pointer'; | ||
| el.innerHTML = '↗'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Avoid innerHTML even for constants (nit).
Use textContent for the marker’s glyph to keep a consistent no-HTML-injection pattern.
- el.innerHTML = '↗';
+ el.textContent = '↗';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| sensors.forEach(sensor => { | |
| const el = document.createElement('div'); | |
| el.className = 'sensor-marker'; | |
| el.style.width = '30px'; | |
| el.style.height = '30px'; | |
| el.style.borderRadius = '50%'; | |
| el.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; | |
| el.style.color = 'white'; | |
| el.style.display = 'flex'; | |
| el.style.justifyContent = 'center'; | |
| el.style.alignItems = 'center'; | |
| el.style.cursor = 'pointer'; | |
| el.innerHTML = '↗'; | |
| sensors.forEach(sensor => { | |
| const el = document.createElement('div'); | |
| el.className = 'sensor-marker'; | |
| el.style.width = '30px'; | |
| el.style.height = '30px'; | |
| el.style.borderRadius = '50%'; | |
| el.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; | |
| el.style.color = 'white'; | |
| el.style.display = 'flex'; | |
| el.style.justifyContent = 'center'; | |
| el.style.alignItems = 'center'; | |
| el.style.cursor = 'pointer'; | |
| el.textContent = '↗'; |
🤖 Prompt for AI Agents
In components/map/mapbox-map.tsx around lines 538 to 551, the marker glyph is
being assigned via el.innerHTML which allows HTML and contradicts the
no-HTML-injection pattern; replace that assignment with el.textContent (or
el.innerText) and use the literal Unicode character (e.g. ↗) or a plain string
so the marker glyph is set as text rather than HTML.
| import { MapToggle } from './map-toggle' | ||
| import { SensorToggle } from './sensor-toggle' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Drop unused/undesirable Map icon import (shadows global).
The file imports Map from lucide-react (Line 10) but doesn’t use it; it also triggers the “shadowing global Map” lint. Remove it.
// Remove Map from this import list:
import {
Search,
CircleUserRound,
- Map,
CalendarDays,
Paperclip,
ArrowRight,
Plus
} from 'lucide-react'🤖 Prompt for AI Agents
In components/mobile-icons-bar.tsx around lines 18 to 19, the import list from
'lucide-react' includes the unused identifier Map which shadows the global Map
and triggers a lint error; remove Map from that import list (so the import only
includes the actually used icons: Search, CircleUserRound, CalendarDays,
Paperclip, ArrowRight, Plus), save the file and run the linter/format step to
ensure no remaining unused-import warnings.
| import { TentTree, RadioTower } from 'lucide-react' | ||
| import { useMapToggle, MapToggleEnum } from './map-toggle-context' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify enum member exists (SensorMode/WebcamMode).
Ensure MapToggleEnum.SensorMode exists. If the intended name is WebcamMode, align the enum and all call sites.
🏁 Script executed:
#!/bin/bash
# Verify MapToggleEnum includes SensorMode or WebcamMode and find call sites.
rg -nP -C2 'enum\s+MapToggleEnum\b|MapToggleEnum\.(SensorMode|WebcamMode)' --type=ts --type=tsxLength of output: 27
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Searching for MapToggleEnum occurrences ==="
rg -n --hidden -S '\bMapToggleEnum\b' || true
echo
echo "=== Searching for enum definition 'enum MapToggleEnum' ==="
rg -n --hidden -S 'enum\s+MapToggleEnum\b' -C3 || true
echo
echo "=== Searching for files named map-toggle-context* ==="
rg -n --hidden -S 'map-toggle-context' || true
echo
echo "=== Attempt to show likely context file(s) (map-toggle-context.*) ==="
for f in $(rg -n --hidden -S --files-with-matches 'map-toggle-context' || true); do
echo
echo "---- File: $f ----"
sed -n '1,240p' "$f" || true
doneLength of output: 16699
Add SensorMode to MapToggleEnum or update the call sites to the correct enum name
MapToggleEnum is currently: FreeMode, RealTimeMode, DrawingMode — but code references MapToggleEnum.SensorMode (or an intended WebcamMode), which will break typechecking/runtime. Either add the missing member to components/map-toggle-context.tsx or change each call site to an existing/intentional enum value.
Locations:
- Add member: components/map-toggle-context.tsx (export enum MapToggleEnum)
- Update usages: components/sensor-toggle.tsx:25; components/map/mapbox-map.tsx:513, components/map/mapbox-map.tsx:533
🤖 Prompt for AI Agents
In components/sensor-toggle.tsx around lines 11-12 (and usages at
components/sensor-toggle.tsx:25, components/map/mapbox-map.tsx:513 & 533), the
code references MapToggleEnum.SensorMode which does not exist on the exported
enum in components/map-toggle-context.tsx; add the missing enum member (e.g.,
SensorMode or WebcamMode) to the export enum MapToggleEnum in
components/map-toggle-context.tsx, or alternatively update each call site to use
an existing/intentional enum value; ensure the chosen enum name is used
consistently across import sites and update any type annotations so TypeScript
compiles.
| <Button variant="ghost" size="icon"> | ||
| <TentTree className="h-[1.2rem] w-[1.2rem]" /> | ||
| </Button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add an accessible name to the icon-only button.
Icon-only controls should expose an aria-label.
- <Button variant="ghost" size="icon">
+ <Button variant="ghost" size="icon" aria-label="Webcams">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Button variant="ghost" size="icon"> | |
| <TentTree className="h-[1.2rem] w-[1.2rem]" /> | |
| </Button> | |
| <Button variant="ghost" size="icon" aria-label="Webcams"> | |
| <TentTree className="h-[1.2rem] w-[1.2rem]" /> | |
| </Button> |
🤖 Prompt for AI Agents
In components/sensor-toggle.tsx around lines 20 to 22, the Button is icon-only
and lacks an accessible name; add an accessible name by supplying an appropriate
aria-label (or aria-labelledby) to the Button (e.g., aria-label="Toggle sensor"
or a label that reflects the specific action/state), or include visually hidden
text inside the button to provide the accessible name; if this is a toggle, also
ensure to expose state via aria-pressed or aria-checked as appropriate.
| <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}> | ||
| <RadioTower className="h-[1rem] w-[1.2rem] mr-2" /> | ||
| Sensors | ||
| </DropdownMenuItem> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick
Align UI text with feature: “Webcams”, not “Sensors”.
PR integrates Windy Webcams; use “Webcams” for clarity. Optionally use a camera icon for the menu item.
- <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}>
- <RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
- Sensors
+ <DropdownMenuItem onClick={() => { setMapType(MapToggleEnum.SensorMode) }}>
+ <RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
+ WebcamsIf you prefer the camera glyph:
- Update imports:
import { TentTree, RadioTower, Camera } from 'lucide-react' - Replace
RadioTowerwithCamera.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}> | |
| <RadioTower className="h-[1rem] w-[1.2rem] mr-2" /> | |
| Sensors | |
| </DropdownMenuItem> | |
| <DropdownMenuItem onClick={() => { setMapType(MapToggleEnum.SensorMode) }}> | |
| <RadioTower className="h-[1rem] w-[1.2rem] mr-2" /> | |
| Webcams | |
| </DropdownMenuItem> |
User description
This change integrates the Windy.com Webcam API into the application. It adds a new "Webcams" mode to the map, which displays webcams from the Windy.com API as markers on the map. Clicking on a marker shows a preview of the webcam stream.
The following changes were made:
WINDY_API_KEYenvironment variable.lib/windy.tsfile to fetch webcam data.WebcamModeto theMapToggleEnum.Tenticon.mapbox-map.tsxcomponent to fetch and display webcams.PR Type
Enhancement
Description
Integrate Windy.com Webcam API for map visualization
Add new WebcamMode to map toggle functionality
Display webcam markers with popup previews
Configure API key environment variables
Diagram Walkthrough
File Walkthrough
windy.ts
New Windy API client implementationlib/windy.ts
getWebcams()function to fetch data from Windy APImap-toggle-context.tsx
Extend map toggle with webcam modecomponents/map-toggle-context.tsx
WebcamModeenum value toMapToggleEnummap-toggle.tsx
Add webcams option to map toggle UIcomponents/map-toggle.tsx
Tenticon from lucide-reactWebcamModetogglemapbox-map.tsx
Integrate webcam markers into Mapbox componentcomponents/map/mapbox-map.tsx
.env
Environment variable for Windy API.env
WINDY_API_KEYenvironment variable configuration.env.local.example
Example environment configuration update.env.local.example
WINDY_API_KEYconfiguration for developersSummary by CodeRabbit
New Features
Documentation
Chores