-
-
Couldn't load subscription status.
- Fork 6
feat: Integrate Windy.com Webcam API #289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cc21fa3
43c19ac
9c1a529
2abf65b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| DATABASE_URL="postgresql://user:password@host:port/db" | ||
| WINDY_API_KEY="YOUR_WINDY_API_KEY" | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,9 @@ | |||||||||
| SMITHERY_PROFILE_ID="your_smithery_profile_id_here" | ||||||||||
| SMITHERY_API_KEY="your_smithery_api_key_here" | ||||||||||
|
|
||||||||||
| # Windy.com Webcams API Key | ||||||||||
| WINDY_API_KEY="your_windy_api_key_here" | ||||||||||
|
|
||||||||||
|
Comment on lines
+6
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🧰 Tools🪛 dotenv-linter (3.3.0)[warning] 7-7: [QuoteCharacter] The value has quote characters (', ") (QuoteCharacter) 🤖 Prompt for AI Agents |
||||||||||
| # NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN is already used by mapbox-map.tsx | ||||||||||
| # Ensure it's also in your .env.local file if you haven't set it up yet. | ||||||||||
| # NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN="your_mapbox_public_token_here" | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,12 +7,12 @@ import { Button } from '@/components/ui/button' | |||||||||||||||
| import { | ||||||||||||||||
| Search, | ||||||||||||||||
| CircleUserRound, | ||||||||||||||||
| Map, | ||||||||||||||||
| CalendarDays, | ||||||||||||||||
| TentTree | ||||||||||||||||
| TentTree, | ||||||||||||||||
| } from 'lucide-react' | ||||||||||||||||
|
Comment on lines
+11
to
12
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| import { MapToggle } from './map-toggle' | ||||||||||||||||
| import { ProfileToggle } from './profile-toggle' | ||||||||||||||||
| import { SensorToggle } from './sensor-toggle' | ||||||||||||||||
|
|
||||||||||||||||
| export const Header = () => { | ||||||||||||||||
| return ( | ||||||||||||||||
|
|
@@ -43,9 +43,7 @@ export const Header = () => { | |||||||||||||||
| <Search className="h-[1.2rem] w-[1.2rem]" /> | ||||||||||||||||
| </Button> | ||||||||||||||||
|
|
||||||||||||||||
| <Button variant="ghost" size="icon"> | ||||||||||||||||
| <TentTree className="h-[1.2rem] w-[1.2rem]" /> | ||||||||||||||||
| </Button> | ||||||||||||||||
| <SensorToggle /> | ||||||||||||||||
|
|
||||||||||||||||
| <ModeToggle /> | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ export enum MapToggleEnum { | |||||||||||||||||
| FreeMode, | ||||||||||||||||||
| RealTimeMode, | ||||||||||||||||||
| DrawingMode, // Added new mode for drawing and measurement | ||||||||||||||||||
| SensorMode, | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+9
to
10
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| interface MapToggleContextType { | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -17,9 +17,8 @@ export function MapToggle() { | |||||
| return ( | ||||||
| <DropdownMenu> | ||||||
| <DropdownMenuTrigger asChild> | ||||||
| <Button variant="ghost" size="icon" className="relative"> | ||||||
| <Map className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||||||
| <span className="sr-only">Toggle map mode</span> | ||||||
| <Button variant="ghost" size="icon"> | ||||||
| <Map className="h-[1.2rem] w-[1.2rem]" /> | ||||||
| </Button> | ||||||
| </DropdownMenuTrigger> | ||||||
| <DropdownMenuContent align="end"> | ||||||
|
|
@@ -30,7 +29,7 @@ export function MapToggle() { | |||||
| My Maps | ||||||
| </DropdownMenuItem> | ||||||
| <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.DrawingMode)}}> | ||||||
| <Pencil className="h-[1rem] w-[1rem] mr-2" /> | ||||||
| <Pencil className="h-[1rem] w-[1.2rem] mr-2" /> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||
| Draw & Measure | ||||||
| </DropdownMenuItem> | ||||||
| </DropdownMenuContent> | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useRef, useCallback } from 'react' // Removed useState | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useRef, useCallback, useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import mapboxgl from 'mapbox-gl' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import MapboxDraw from '@mapbox/mapbox-gl-draw' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as turf from '@turf/turf' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -11,6 +11,7 @@ import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useMapToggle, MapToggleEnum } from '../map-toggle-context' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useMapData } from './map-data-context'; // Add this import | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useMapLoading } from '../map-loading-context'; // Import useMapLoading | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getSensors, Sensor } from '@/lib/sensors'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -32,6 +33,9 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { mapData, setMapData } = useMapData(); // Consume the new context, get setMapData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { setIsMapLoaded } = useMapLoading(); // Get setIsMapLoaded from context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const previousMapTypeRef = useRef<MapToggleEnum | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [sensors, setSensors] = useState<Sensor[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sensorMarkersRef = useRef<{ [id: string]: mapboxgl.Marker }>({}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Refs for long-press functionality | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const longPressTimerRef = useRef<NodeJS.Timeout | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -505,6 +509,58 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [position, updateMapPosition, mapType]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (map.current && mapType === MapToggleEnum.SensorMode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clear existing markers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.values(sensorMarkersRef.current).forEach(marker => marker.remove()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sensorMarkersRef.current = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 = '↗'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+538
to
+551
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Avoid innerHTML even for constants (nit). Use textContent for the marker’s glyph to keep a consistent no-HTML-injection pattern. - el.innerHTML = '↗';
+ el.textContent = '↗';📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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!); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sensorMarkersRef.current[sensor.id] = marker; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [sensors, mapType]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Effect to handle map updates from MapDataContext | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (mapData.targetPosition && map.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,14 +9,14 @@ import { | |
| CircleUserRound, | ||
| Map, | ||
| CalendarDays, | ||
| TentTree, | ||
| Paperclip, | ||
| ArrowRight, | ||
| Plus | ||
| } from 'lucide-react' | ||
| import { History } from '@/components/history' | ||
| import { MapToggle } from './map-toggle' | ||
| import { ModeToggle } from './mode-toggle' | ||
| import { MapToggle } from './map-toggle' | ||
| import { SensorToggle } from './sensor-toggle' | ||
|
Comment on lines
+18
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Drop unused/undesirable Map icon import (shadows global). The file imports // Remove Map from this import list:
import {
Search,
CircleUserRound,
- Map,
CalendarDays,
Paperclip,
ArrowRight,
Plus
} from 'lucide-react'🤖 Prompt for AI Agents |
||
|
|
||
| export const MobileIconsBar: React.FC = () => { | ||
| const [, setMessages] = useUIState<typeof AI>() | ||
|
|
@@ -42,9 +42,7 @@ export const MobileIconsBar: React.FC = () => { | |
| <Button variant="ghost" size="icon"> | ||
| <Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| </Button> | ||
| <Button variant="ghost" size="icon"> | ||
| <TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| </Button> | ||
| <SensorToggle /> | ||
| <Button variant="ghost" size="icon"> | ||
| <Paperclip className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" /> | ||
| </Button> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||||||||||
| 'use client' | ||||||||||||||||||
|
|
||||||||||||||||||
| import * as React from 'react' | ||||||||||||||||||
| import { Button } from '@/components/ui/button' | ||||||||||||||||||
| import { | ||||||||||||||||||
| DropdownMenu, | ||||||||||||||||||
| DropdownMenuContent, | ||||||||||||||||||
| DropdownMenuItem, | ||||||||||||||||||
| DropdownMenuTrigger | ||||||||||||||||||
| } from '@/components/ui/dropdown-menu' | ||||||||||||||||||
| import { TentTree, RadioTower } from 'lucide-react' | ||||||||||||||||||
| import { useMapToggle, MapToggleEnum } from './map-toggle-context' | ||||||||||||||||||
|
Comment on lines
+11
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify enum member exists (SensorMode/WebcamMode). Ensure 🏁 Script executed: #!/bin/bash
# Verify MapToggleEnum includes SensorMode or WebcamMode and find call sites.
rg -nP -C2 'enum\s+MapToggleEnum\b|MapToggleEnum\.(SensorMode|WebcamMode)' --type=ts --type=tsxLength of output: 27 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Searching for MapToggleEnum occurrences ==="
rg -n --hidden -S '\bMapToggleEnum\b' || true
echo
echo "=== Searching for enum definition 'enum MapToggleEnum' ==="
rg -n --hidden -S 'enum\s+MapToggleEnum\b' -C3 || true
echo
echo "=== Searching for files named map-toggle-context* ==="
rg -n --hidden -S 'map-toggle-context' || true
echo
echo "=== Attempt to show likely context file(s) (map-toggle-context.*) ==="
for f in $(rg -n --hidden -S --files-with-matches 'map-toggle-context' || true); do
echo
echo "---- File: $f ----"
sed -n '1,240p' "$f" || true
doneLength of output: 16699 Add SensorMode to MapToggleEnum or update the call sites to the correct enum name MapToggleEnum is currently: FreeMode, RealTimeMode, DrawingMode — but code references MapToggleEnum.SensorMode (or an intended WebcamMode), which will break typechecking/runtime. Either add the missing member to components/map-toggle-context.tsx or change each call site to an existing/intentional enum value. Locations:
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| export function SensorToggle() { | ||||||||||||||||||
| const { setMapType } = useMapToggle(); | ||||||||||||||||||
|
|
||||||||||||||||||
| return ( | ||||||||||||||||||
| <DropdownMenu> | ||||||||||||||||||
| <DropdownMenuTrigger asChild> | ||||||||||||||||||
| <Button variant="ghost" size="icon"> | ||||||||||||||||||
| <TentTree className="h-[1.2rem] w-[1.2rem]" /> | ||||||||||||||||||
| </Button> | ||||||||||||||||||
|
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| </DropdownMenuTrigger> | ||||||||||||||||||
| <DropdownMenuContent align="end"> | ||||||||||||||||||
| <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}> | ||||||||||||||||||
| <RadioTower className="h-[1rem] w-[1.2rem] mr-2" /> | ||||||||||||||||||
| Sensors | ||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||
|
Comment on lines
+25
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick Align UI text with feature: “Webcams”, not “Sensors”. PR integrates Windy Webcams; use “Webcams” for clarity. Optionally use a camera icon for the menu item. - <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}>
- <RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
- Sensors
+ <DropdownMenuItem onClick={() => { setMapType(MapToggleEnum.SensorMode) }}>
+ <RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
+ WebcamsIf you prefer the camera glyph:
📝 Committable suggestion
Suggested change
|
||||||||||||||||||
| </DropdownMenuContent> | ||||||||||||||||||
| </DropdownMenu> | ||||||||||||||||||
| ) | ||||||||||||||||||
| } | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface Sensor { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| location: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| latitude: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| longitude: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| images: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| current: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| preview: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| daylight: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| preview: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| urls: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| webcam: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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[];
}
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface WindyResponse { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| webcams: Sensor[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainConfirm 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: 💡 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 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.
Location: lib/sensors.ts (lines 21–23) |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getSensors(): Promise<WindyResponse> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = process.env.WINDY_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('WINDY_API_KEY is not defined or not set in the environment variables.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Secret usage in a client-importable module. This module is imported by a client component (Mapbox). Accessing process.env.WINDY_API_KEY here will either leak the key (if bundled) or be undefined at runtime. Fetch secrets server-side only and expose a safe API route. Apply server-only guard and move fetch to an API route; then have clients call the route: +import 'server-only';Example API route (add new file app/api/sensors/route.ts): // app/api/sensors/route.ts
import 'server-only';
export const dynamic = 'force-dynamic';
export async function GET(req: Request) {
const apiKey = process.env.WINDY_API_KEY;
if (!apiKey) {
return new Response(JSON.stringify({ error: 'Missing WINDY_API_KEY' }), { status: 500 });
}
const { searchParams } = new URL(req.url);
const bbox = searchParams.get('bbox') ?? '';
const url = new URL('https://api.windy.com/webcams/api/v3/webcams');
url.searchParams.set('include', 'location,images,urls');
if (bbox) url.searchParams.set('bbox', bbox);
const ac = new AbortController();
const to = setTimeout(() => ac.abort(), 10000);
try {
const res = await fetch(url.toString(), {
headers: { 'x-windy-api-key': apiKey },
signal: ac.signal,
cache: 'no-store',
});
if (!res.ok) {
const txt = await res.text();
return new Response(JSON.stringify({ error: txt || res.statusText }), { status: res.status });
}
const json = await res.json();
return new Response(JSON.stringify(json), { status: 200 });
} catch (e) {
const msg = (e as Error)?.message ?? 'unknown error';
return new Response(JSON.stringify({ error: msg }), { status: 500 });
} finally {
clearTimeout(to);
}
}Then update the client to fetch /api/sensors (see mapbox-map.tsx comments). |
||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const errorText = await response.text(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to fetch sensors:', response.statusText, errorText); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Failed to fetch sensors: ${response.statusText}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Successfully fetched sensor data:', data); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return data; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+49
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Error in getSensors:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Do not quote placeholder values; keep style consistent with dotenv-linter.
Unquote to satisfy the linter and match common dotenv style.
📝 Committable suggestion
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 2-2: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
🤖 Prompt for AI Agents