Skip to content

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Sep 15, 2025

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:

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

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

flowchart LR
  A["Environment Config"] --> B["Windy API Client"]
  B --> C["Webcam Data Fetching"]
  C --> D["Map Toggle Context"]
  D --> E["Mapbox Map Component"]
  E --> F["Webcam Markers Display"]
  F --> G["Interactive Popups"]
Loading

File Walkthrough

Relevant files
Enhancement
windy.ts
New Windy API client implementation                                           

lib/windy.ts

  • Create TypeScript interfaces for Webcam and WindyResponse
  • Implement getWebcams() function to fetch data from Windy API
  • Add error handling for missing API key and failed requests
+40/-0   
map-toggle-context.tsx
Extend map toggle with webcam mode                                             

components/map-toggle-context.tsx

  • Add WebcamMode enum value to MapToggleEnum
+1/-0     
map-toggle.tsx
Add webcams option to map toggle UI                                           

components/map-toggle.tsx

  • Import Tent icon from lucide-react
  • Add "Webcams" dropdown menu item with tent icon
  • Connect webcam option to WebcamMode toggle
+5/-1     
mapbox-map.tsx
Integrate webcam markers into Mapbox component                     

components/map/mapbox-map.tsx

  • Import webcam-related modules and add state management
  • Implement webcam fetching effect for WebcamMode
  • Create webcam markers with camera icons and popups
  • Add marker cleanup when switching modes
+53/-1   
Configuration changes
.env
Environment variable for Windy API                                             

.env

  • Add WINDY_API_KEY environment variable configuration
+1/-0     
Documentation
.env.local.example
Example environment configuration update                                 

.env.local.example

  • Add example WINDY_API_KEY configuration for developers
  • Include documentation comment for Windy.com Webcams API
+3/-0     

Summary by CodeRabbit

  • New Features

    • Added a Sensors map mode showing per-sensor markers with image previews and links; accessible via a new Sensors toggle in header and mobile bar.
  • Documentation

    • Added WINDY_API_KEY to example environment files to enable Sensors mode.
  • Chores

    • App now requires a valid Windy API key for Sensors mode to fetch webcam data.

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.
@vercel
Copy link

vercel bot commented Sep 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
qcx Ready Ready Preview Comment Sep 24, 2025 11:05am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Environment samples
\.env, \.env.local.example
Added WINDY_API_KEY placeholder/sample entries; no removals or other edits to existing variables.
Map mode enum & toggle visuals
components/map-toggle-context.tsx, components/map-toggle.tsx
Added MapToggleEnum.SensorMode. Minor UI class/structural tweaks to the map toggle button/icon (CSS class removals and icon size change).
Sensor toggle UI
components/sensor-toggle.tsx, components/header.tsx, components/mobile-icons-bar.tsx
New SensorToggle component added and imported into header and mobile icons bar; replaced previous TentTree button usage (TentTree import removed). SensorToggle sets map type to SensorMode.
Mapbox sensor integration
components/map/mapbox-map.tsx
On SensorMode enter: call getSensors(), store sensors state, create/attach per-sensor DOM markers (30×30 circular elements) with Popups linking to webcams and preview images, track markers in sensorMarkersRef, and remove/cleanup markers and sensors on mode switch or updates.
Sensors data module
lib/sensors.ts
New exported types Sensor and WindyResponse and getSensors() function. Validates WINDY_API_KEY, performs fetch to Windy Webcams API with x-windy-api-key header, handles non-OK responses, logs progress/errors, and returns parsed JSON.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitch my nose at windy skies,
Small towers blink where webcam lies.
A key, a toggle—markers spring,
I hop, I peer, I hear them sing.
Sensors on the map, I bound—hooray! 🐇📡

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: Integrate Windy.com Webcam API" is a concise, single-sentence summary that directly reflects the main change in the PR — adding Windy webcam integration including env var, API client, and map UI/mode updates; it is specific and free of noise. It clearly communicates the primary intent to reviewers scanning the repository history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/windy-webcam-integration

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.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@qodo-merge-pro
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 Security concerns

XSS risk:
Popups are created with setHTML using API-derived fields (title, image URL). If the API data is compromised or not sanitized, it could inject scripts. Use DOM APIs instead of setHTML or sanitize the content.

⚡ Recommended focus areas for review

API URL/Schema Assumption

The Windy API endpoint and response shape are hardcoded and may not match the real API (path, required query params, and JSON schema). Validate the endpoint, required parameters (e.g., fields, bounding box, limit), and that response.json() matches WindyResponse to avoid runtime failures.

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,
    },
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch webcams: ${response.statusText}`);
  }

  return response.json();
Marker Cleanup

Markers are cleared on mode switch and before re-rendering in WebcamMode, but not on component unmount. Add a cleanup to remove any remaining webcam markers to prevent memory leaks when the map unmounts.

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]);
Unsafe HTML in Popup

Popups use setHTML with interpolated values from the API. If titles or image URLs are not trusted/sanitized, this could enable XSS. Prefer creating DOM nodes or sanitizing content before injection.

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!);

@qodo-merge-pro
Copy link
Contributor

qodo-merge-pro bot commented Sep 15, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Refactor to fetch data via a server-side API route

Move the Windy API call from the client-side component to a new server-side API
route. This change is crucial to prevent exposing the secret API key to the
browser.

Examples:

components/map/mapbox-map.tsx [512-530]
  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.');
        }

 ... (clipped 9 lines)
lib/windy.ts [22-40]
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,

 ... (clipped 9 lines)

Solution Walkthrough:

Before:

// components/map/mapbox-map.tsx ('use client')
import { getWebcams } from '@/lib/windy';

useEffect(() => {
  if (mapType === MapToggleEnum.WebcamMode) {
    const fetchWebcams = async () => {
      // This function is called on the client
      const windyResponse = await getWebcams();
      setWebcams(windyResponse.webcams);
    };
    fetchWebcams();
  }
}, [mapType]);

// lib/windy.ts
export async function getWebcams() {
  // process.env.WINDY_API_KEY is a server-side variable
  // and will be undefined when this code runs in the browser.
  const apiKey = process.env.WINDY_API_KEY;
  const response = await fetch('https://api.windy.com/...', {
    headers: { 'x-windy-api-key': apiKey },
  });
  return response.json();
}

After:

// app/api/webcams/route.ts (New Server-Side Route)
export async function GET() {
  const apiKey = process.env.WINDY_API_KEY;
  if (!apiKey) {
    // Handle error
  }
  // This fetch call now runs securely on the server
  const response = await fetch('https://api.windy.com/...', {
    headers: { 'x-windy-api-key': apiKey },
  });
  const data = await response.json();
  return Response.json(data);
}

// components/map/mapbox-map.tsx ('use client')
useEffect(() => {
  if (mapType === MapToggleEnum.WebcamMode) {
    const fetchWebcams = async () => {
      // Fetch from the internal server-side API route
      const response = await fetch('/api/webcams');
      const data = await response.json();
      setWebcams(data.webcams);
    };
    fetchWebcams();
  }
}, [mapType]);
Suggestion importance[1-10]: 10

__

Why: This suggestion correctly identifies a critical security vulnerability where a secret API key would be exposed to the client, which is a major design flaw.

High
Security
Prevent API key leakage to client

Move the getWebcams API call to a server-side API route to avoid exposing the
WINDY_API_KEY on the client. The client component should then fetch data from
this new internal endpoint.

components/map/mapbox-map.tsx [512-530]

 useEffect(() => {
   if (mapType === MapToggleEnum.WebcamMode) {
     const fetchWebcams = async () => {
       try {
-        const windyResponse = await getWebcams();
+        const response = await fetch('/api/webcams');
+        if (!response.ok) {
+          throw new Error(`Server responded with ${response.status}`);
+        }
+        const windyResponse = await response.json();
         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]);
  • Apply / Chat
Suggestion importance[1-10]: 10

__

Why: The suggestion correctly identifies a critical security vulnerability where a secret API key is exposed to the client-side and proposes the standard, correct architectural fix.

High
Prevent XSS in map popups

To prevent a Cross-Site Scripting (XSS) vulnerability, use setDOMContent with
programmatically created DOM elements instead of injecting raw HTML with setHTML
for the map popup.

components/map/mapbox-map.tsx [548-553]

+const popupNode = document.createElement('div');
+const titleEl = document.createElement('h3');
+titleEl.textContent = webcam.title; // Safely sets text content
+const imgEl = document.createElement('img');
+imgEl.src = webcam.images.current.preview;
+imgEl.alt = webcam.title;
+imgEl.style.width = '100%';
+popupNode.appendChild(titleEl);
+popupNode.appendChild(imgEl);
+
 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(popupNode))
   .addTo(map.current!);
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical Cross-Site Scripting (XSS) vulnerability and provides a robust solution using setDOMContent to prevent arbitrary code execution.

High
  • Update

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between f3df729 and cc21fa3.

📒 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"
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +6 to +8
# Windy.com Webcams API Key
WINDY_API_KEY="your_windy_api_key_here"

Copy link
Contributor

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.

Suggested change
# 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.

DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Map, Pencil } from 'lucide-react'
import { Map, Pencil, Tent } from 'lucide-react'
Copy link
Contributor

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.

Suggested change
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.

Comment on lines 14 to 15
import { getWebcams, Webcam } from '@/lib/windy';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 36 to 38
const [webcams, setWebcams] = useState<Webcam[]>([]);
const webcamMarkersRef = useRef<{ [id: string]: mapboxgl.Marker }>({});

Copy link
Contributor

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.

Comment on lines 532 to 559
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]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 539 to 547
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';

Copy link
Contributor

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
Comment on lines 22 to 34
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,
},
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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.
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between cc21fa3 and 43c19ac.

📒 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.

Comment on lines 512 to 559
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]);

Copy link
Contributor

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
Comment on lines 1 to 19
export interface Webcam {
id: string;
title: string;
location: {
latitude: number;
longitude: number;
};
images: {
current: {
preview: string;
};
daylight: {
preview: string;
}
};
urls: {
webcam: string;
};
}
Copy link
Contributor

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.

Suggested change
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
Comment on lines 25 to 36
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,
},
});
Copy link
Contributor

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
Comment on lines 31 to 36
const response = await fetch('https://api.windy.com/webcams/api/v3/webcams', {
method: 'GET',
headers: {
'x-windy-api-key': apiKey,
},
});
Copy link
Contributor

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.

Suggested change
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
Comment on lines 38 to 43
if (!response.ok) {
throw new Error(`Failed to fetch webcams: ${response.statusText}`);
}

return response.json();
}
Copy link
Contributor

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.

Suggested change
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`
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 43c19ac and 9c1a529.

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

Comment on lines +9 to 10
SensorMode,
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines 32 to 35
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}>
<RadioTower className="h-[1rem] w-[1rem] mr-2" />
Sensors
</DropdownMenuItem>
Copy link
Contributor

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.

Suggested change
<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.

Comment on lines +1 to +19
export interface Sensor {
id: string;
title: string;
location: {
latitude: number;
longitude: number;
};
images: {
current: {
preview: string;
};
daylight: {
preview: string;
}
};
urls: {
webcam: string;
};
}
Copy link
Contributor

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.

Comment on lines +21 to +23
export interface WindyResponse {
webcams: Sensor[];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

Comment on lines +25 to +31
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.');
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

Comment on lines +27 to +30
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.');
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +33 to +41
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);
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +49 to +51
const data = await response.json();
console.log('Successfully fetched sensor data:', data);
return data;
Copy link
Contributor

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.

Suggested change
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.
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9c1a529 and 2abf65b.

📒 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 Map import to MapIcon and 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.

Comment on lines +11 to 12
TentTree,
} from 'lucide-react'
Copy link
Contributor

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.

Suggested change
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" />
Copy link
Contributor

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.

Suggested change
<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.

Comment on lines +538 to +551
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 = '&#8599;';

Copy link
Contributor

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 = '&#8599;';
+        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.

Suggested change
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 = '&#8599;';
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.

Comment on lines +18 to +19
import { MapToggle } from './map-toggle'
import { SensorToggle } from './sensor-toggle'
Copy link
Contributor

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.

Comment on lines +11 to +12
import { TentTree, RadioTower } from 'lucide-react'
import { useMapToggle, MapToggleEnum } from './map-toggle-context'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 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=tsx

Length 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
done

Length 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.

Comment on lines +20 to +22
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
Copy link
Contributor

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.

Suggested change
<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.

Comment on lines +25 to +28
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}>
<RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
Sensors
</DropdownMenuItem>
Copy link
Contributor

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" />
+          Webcams

If you prefer the camera glyph:

  • Update imports: import { TentTree, RadioTower, Camera } from 'lucide-react'
  • Replace RadioTower with Camera.
📝 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.

Suggested change
<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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants