folx-tv/client/src/components/weather-widget.tsx
sebastjanartic fd7ecad753 Make weather widget more compact and informative
Update the weather widget component to display temperature, weather description, city, wind speed, and humidity in a more compact, single-line layout.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d391d31b-d28a-42c0-884b-d4aa19bfcf94
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/0ZGabQy
Replit-Helium-Checkpoint-Created: true
2026-02-28 21:52:06 +00:00

128 lines
5.0 KiB
TypeScript

import { useState, useEffect } from "react";
import { Cloud, Sun, CloudRain, CloudSnow, CloudLightning, CloudDrizzle, Wind, Droplets, Thermometer, MapPin } from "lucide-react";
interface WeatherData {
temperature: number;
weatherCode: number;
windSpeed: number;
humidity: number;
city: string;
}
function getWeatherIcon(code: number) {
if (code === 0 || code === 1) return <Sun className="w-8 h-8 text-yellow-400" />;
if (code === 2 || code === 3) return <Cloud className="w-8 h-8 text-gray-400" />;
if (code >= 51 && code <= 57) return <CloudDrizzle className="w-8 h-8 text-blue-300" />;
if (code >= 61 && code <= 67) return <CloudRain className="w-8 h-8 text-blue-400" />;
if (code >= 71 && code <= 77) return <CloudSnow className="w-8 h-8 text-white" />;
if (code >= 80 && code <= 82) return <CloudRain className="w-8 h-8 text-blue-500" />;
if (code >= 95) return <CloudLightning className="w-8 h-8 text-yellow-500" />;
return <Cloud className="w-8 h-8 text-gray-400" />;
}
function getWeatherText(code: number): string {
if (code === 0) return "Klar";
if (code === 1) return "Überwiegend klar";
if (code === 2) return "Teilweise bewölkt";
if (code === 3) return "Bewölkt";
if (code >= 51 && code <= 57) return "Nieselregen";
if (code >= 61 && code <= 65) return "Regen";
if (code === 66 || code === 67) return "Gefrierender Regen";
if (code >= 71 && code <= 75) return "Schneefall";
if (code === 77) return "Schneegriesel";
if (code >= 80 && code <= 82) return "Regenschauer";
if (code === 85 || code === 86) return "Schneeschauer";
if (code >= 95) return "Gewitter";
return "Unbekannt";
}
async function reverseGeocode(lat: number, lon: number): Promise<string> {
try {
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=10&accept-language=de`);
const data = await res.json();
return data.address?.city || data.address?.town || data.address?.village || data.address?.municipality || "Unbekannt";
} catch {
return "Unbekannt";
}
}
export function WeatherWidget() {
const [weather, setWeather] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
const defaultLat = 46.55;
const defaultLon = 14.55;
const defaultCity = "Klagenfurt";
async function fetchWeather(lat: number, lon: number, city: string) {
try {
const res = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,weather_code,wind_speed_10m,relative_humidity_2m&timezone=auto`
);
const data = await res.json();
setWeather({
temperature: Math.round(data.current.temperature_2m),
weatherCode: data.current.weather_code,
windSpeed: Math.round(data.current.wind_speed_10m),
humidity: data.current.relative_humidity_2m,
city,
});
} catch {
setError(true);
} finally {
setLoading(false);
}
}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
async (pos) => {
const city = await reverseGeocode(pos.coords.latitude, pos.coords.longitude);
fetchWeather(pos.coords.latitude, pos.coords.longitude, city);
},
() => {
fetchWeather(defaultLat, defaultLon, defaultCity);
},
{ timeout: 5000 }
);
} else {
fetchWeather(defaultLat, defaultLon, defaultCity);
}
}, []);
if (loading) {
return (
<div className="bg-card rounded-lg border border-card-border p-4 h-full" data-testid="widget-weather-loading">
<div className="flex items-center gap-2 mb-3">
<Thermometer className="w-4 h-4 text-primary" />
<h3 className="font-bold text-card-foreground text-sm">Wetter</h3>
</div>
<div className="h-16 bg-muted animate-pulse rounded" />
</div>
);
}
if (error || !weather) return null;
return (
<div className="bg-card rounded-lg border border-card-border px-3 py-2" data-testid="widget-weather">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">{getWeatherIcon(weather.weatherCode)}</div>
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-1.5">
<span className="text-lg font-bold text-card-foreground">{weather.temperature}°C</span>
<span className="text-[10px] text-muted-foreground">{getWeatherText(weather.weatherCode)}</span>
</div>
<div className="flex items-center gap-3 text-[10px] text-muted-foreground">
<span className="flex items-center gap-0.5"><MapPin className="w-2.5 h-2.5" />{weather.city}</span>
<span className="flex items-center gap-0.5"><Wind className="w-2.5 h-2.5" />{weather.windSpeed} km/h</span>
<span className="flex items-center gap-0.5"><Droplets className="w-2.5 h-2.5" />{weather.humidity}%</span>
</div>
</div>
</div>
</div>
);
}