From 234bb25c1ec36dd9e3cdd840797efd30c6624e5c Mon Sep 17 00:00:00 2001 From: mARTin Date: Thu, 19 Mar 2026 22:44:04 +0100 Subject: [PATCH] feat: configurable weather display data per view Users can now pick which weather data to show per time slot: icon, temperature, feels like, wind speed, gusts, rain probability, precipitation, humidity, and UV index. API now fetches all data from Open-Meteo. Also fixed slot height for 20min duration. v1.52.0 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/app/api/weather/route.ts | 22 ++++++++---- src/components/WeeklyView.tsx | 65 ++++++++++++++++++++++++++++++----- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 44e5f3c..8076536 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.51.6", + "version": "1.52.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/api/weather/route.ts b/src/app/api/weather/route.ts index 97323dd..f7dc97a 100644 --- a/src/app/api/weather/route.ts +++ b/src/app/api/weather/route.ts @@ -36,7 +36,7 @@ export async function GET(request: NextRequest) { } try { - const url = `https://api.open-meteo.com/v1/forecast?latitude=${user.weatherLat}&longitude=${user.weatherLon}&hourly=temperature_2m,weather_code&start_date=${startDate}&end_date=${endDate}&timezone=auto`; + const url = `https://api.open-meteo.com/v1/forecast?latitude=${user.weatherLat}&longitude=${user.weatherLon}&hourly=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,wind_gusts_10m,precipitation_probability,precipitation,relative_humidity_2m,uv_index&start_date=${startDate}&end_date=${endDate}&timezone=auto`; const res = await fetch(url, { next: { revalidate: 900 } }); if (!res.ok) { @@ -45,13 +45,23 @@ export async function GET(request: NextRequest) { const raw = await res.json(); - // Transform into { "2026-03-17T08:00": { temp: 5, code: 2 }, ... } - const hourly: Record = {}; - if (raw.hourly?.time && raw.hourly?.temperature_2m && raw.hourly?.weather_code) { + const hourly: Record = {}; + if (raw.hourly?.time) { for (let i = 0; i < raw.hourly.time.length; i++) { hourly[raw.hourly.time[i]] = { - temp: Math.round(raw.hourly.temperature_2m[i]), - code: raw.hourly.weather_code[i], + temp: Math.round(raw.hourly.temperature_2m?.[i] ?? 0), + code: raw.hourly.weather_code?.[i] ?? 0, + feelsLike: raw.hourly.apparent_temperature?.[i] != null ? Math.round(raw.hourly.apparent_temperature[i]) : undefined, + wind: raw.hourly.wind_speed_10m?.[i] != null ? Math.round(raw.hourly.wind_speed_10m[i]) : undefined, + gusts: raw.hourly.wind_gusts_10m?.[i] != null ? Math.round(raw.hourly.wind_gusts_10m[i]) : undefined, + precipProb: raw.hourly.precipitation_probability?.[i] ?? undefined, + precip: raw.hourly.precipitation?.[i] != null ? Math.round(raw.hourly.precipitation[i] * 10) / 10 : undefined, + humidity: raw.hourly.relative_humidity_2m?.[i] ?? undefined, + uv: raw.hourly.uv_index?.[i] != null ? Math.round(raw.hourly.uv_index[i] * 10) / 10 : undefined, }; } } diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 2e8cf4a..c723a35 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -178,6 +178,8 @@ interface SomedayList { // Time grid configuration options type CellDuration = 15 | 20 | 30 | 60; +type WeatherDisplayKey = "icon" | "temp" | "feelsLike" | "wind" | "gusts" | "precipProb" | "precip" | "humidity" | "uv"; +const WEATHER_DISPLAY_DEFAULTS: WeatherDisplayKey[] = ["icon", "temp"]; const DEFAULT_SOMEDAY_SLOT_COUNT = 5; const getSomedaySlotCount = (tasks: Task[]) => { @@ -1572,8 +1574,9 @@ export default function WeeklyView() { [], ); - // Weather data: { "2026-03-17T08:00": { temp: 5, code: 2 }, ... } - const [weatherData, setWeatherData] = useState>({}); + // Weather data: { "2026-03-17T08:00": { temp: 5, code: 2, wind: 12, ... }, ... } + type WeatherHour = { temp: number; code: number; feelsLike?: number; wind?: number; gusts?: number; precipProb?: number; precip?: number; humidity?: number; uv?: number }; + const [weatherData, setWeatherData] = useState>({}); // Extend events with editable flag from connections const calendarEvents = useMemo(() => { @@ -1958,6 +1961,7 @@ export default function WeeklyView() { hourLabelFormat?: "short" | "full"; showSubHourSlots?: boolean; weatherEnabled?: boolean; + weatherDisplay?: WeatherDisplayKey[]; showTaskCheckboxes?: boolean; showSomeday?: boolean; showAllDayEvents?: boolean; @@ -1967,7 +1971,7 @@ export default function WeeklyView() { startHour?: number; endHour?: number; }; - const PER_VIEW_KEYS = ["hourLabelFormat", "showSubHourSlots", "weatherEnabled", "showTaskCheckboxes", "showSomeday", "showAllDayEvents", "allDayPosition", "showCompletedTasks", "cellDuration", "startHour", "endHour"] as const; + const PER_VIEW_KEYS = ["hourLabelFormat", "showSubHourSlots", "weatherEnabled", "weatherDisplay", "showTaskCheckboxes", "showSomeday", "showAllDayEvents", "allDayPosition", "showCompletedTasks", "cellDuration", "startHour", "endHour"] as const; const [viewSettings, setViewSettings] = useState>({}); const viewSettingsRef = useRef>({}); viewSettingsRef.current = viewSettings; @@ -2036,6 +2040,7 @@ export default function WeeklyView() { const effectiveHourLabelFormat = getEffective("hourLabelFormat", hourLabelFormat); const effectiveShowSubHourSlots = getEffective("showSubHourSlots", showSubHourSlots); const effectiveWeatherEnabled = getEffective("weatherEnabled", profile.weatherEnabled); + const effectiveWeatherDisplay = (getEffective("weatherDisplay", WEATHER_DISPLAY_DEFAULTS) || WEATHER_DISPLAY_DEFAULTS) as WeatherDisplayKey[]; const effectiveShowTaskCheckboxes = getEffective("showTaskCheckboxes", profile.showTaskCheckboxes); const effectiveShowSomeday = getEffective("showSomeday", showSomeday); const effectiveShowAllDay = getEffective("showAllDayEvents", showAllDay); @@ -2410,9 +2415,9 @@ export default function WeeklyView() { const getSlotHeight = (duration: number) => { switch (duration) { case 15: return 25; + case 20: return 30; case 30: return 35; case 60: return 50; - case 120: return 80; default: return 50; } }; @@ -7056,11 +7061,20 @@ export default function WeeklyView() { const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}T${String(h).padStart(2, "0")}:00`; const w = weatherData[dateStr]; if (!w) return null; - const icon = getWeatherIcon(w.code); + const d = effectiveWeatherDisplay; + const parts: string[] = []; + if (d.includes("temp")) parts.push(`${w.temp}°`); + if (d.includes("feelsLike") && w.feelsLike != null) parts.push(`(${w.feelsLike}°)`); + if (d.includes("wind") && w.wind != null) parts.push(`${w.wind}km/h`); + if (d.includes("gusts") && w.gusts != null) parts.push(`💨${w.gusts}`); + if (d.includes("precipProb") && w.precipProb != null) parts.push(`${w.precipProb}%`); + if (d.includes("precip") && w.precip != null && w.precip > 0) parts.push(`${w.precip}mm`); + if (d.includes("humidity") && w.humidity != null) parts.push(`💧${w.humidity}%`); + if (d.includes("uv") && w.uv != null && w.uv > 0) parts.push(`UV${w.uv}`); return ( -
- {icon} - {w.temp}° +
+ {d.includes("icon") && {getWeatherIcon(w.code)}} + {parts.length > 0 && {parts.join(" ")}}
); })()} @@ -11199,6 +11213,41 @@ function SettingsSidebar({
)} + {/* Weather display options */} +
+
+ {profile.language === "de" ? "Angezeigte Daten" : "Display data"} +
+ {([ + { key: "icon" as WeatherDisplayKey, label: profile.language === "de" ? "Wettersymbol" : "Weather icon", icon: "☀️" }, + { key: "temp" as WeatherDisplayKey, label: profile.language === "de" ? "Temperatur" : "Temperature", icon: "🌡️" }, + { key: "feelsLike" as WeatherDisplayKey, label: profile.language === "de" ? "Gefühlte Temp." : "Feels like", icon: "🤒" }, + { key: "wind" as WeatherDisplayKey, label: profile.language === "de" ? "Windgeschwindigkeit" : "Wind speed", icon: "🌬️" }, + { key: "gusts" as WeatherDisplayKey, label: profile.language === "de" ? "Windböen" : "Wind gusts", icon: "💨" }, + { key: "precipProb" as WeatherDisplayKey, label: profile.language === "de" ? "Regenwahrscheinl." : "Rain probability", icon: "🌧️" }, + { key: "precip" as WeatherDisplayKey, label: profile.language === "de" ? "Niederschlag (mm)" : "Precipitation (mm)", icon: "💦" }, + { key: "humidity" as WeatherDisplayKey, label: profile.language === "de" ? "Luftfeuchtigkeit" : "Humidity", icon: "💧" }, + { key: "uv" as WeatherDisplayKey, label: "UV Index", icon: "☀️" }, + ]).map(({ key, label, icon }) => { + const current = (perView.getEffective("weatherDisplay", WEATHER_DISPLAY_DEFAULTS) || WEATHER_DISPLAY_DEFAULTS) as WeatherDisplayKey[]; + const checked = current.includes(key); + return ( + + ); + })} +
); })()}