Add automatic IP-based weather detection to a new sidebar widget

Introduces a `SidebarWeatherWidget` component that displays localized weather information using IP address for location detection, removing the need for explicit user permission. The widget is placed in the homepage sidebar below the "Zuletzt hinzugefügt" section.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fbb31221-fc0e-43bb-bb7c-88ad38d97042
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/nFw7xof
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic 2026-03-02 17:02:16 +00:00
parent e0a9faf61c
commit 1fecc39035
2 changed files with 121 additions and 14 deletions

View File

@ -76,20 +76,22 @@ export function WeatherWidget() {
}
}
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);
},
() => {
async function getLocationByIP() {
try {
const res = await fetch("https://ipapi.co/json/");
const data = await res.json();
if (data.latitude && data.longitude) {
const city = data.city || await reverseGeocode(data.latitude, data.longitude);
fetchWeather(data.latitude, data.longitude, city);
} else {
fetchWeather(defaultLat, defaultLon, defaultCity);
},
{ timeout: 5000 }
);
} else {
fetchWeather(defaultLat, defaultLon, defaultCity);
}
} catch {
fetchWeather(defaultLat, defaultLon, defaultCity);
}
}
getLocationByIP();
}, []);
if (loading) {
@ -125,3 +127,107 @@ export function WeatherWidget() {
</div>
);
}
export function SidebarWeatherWidget() {
const [weather, setWeather] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
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&daily=temperature_2m_max,temperature_2m_min,weather_code&timezone=auto&forecast_days=3`
);
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,
});
setForecast(data.daily ? data.daily.time.slice(1, 3).map((d: string, i: number) => ({
day: new Date(d).toLocaleDateString("de-DE", { weekday: "short" }),
high: Math.round(data.daily.temperature_2m_max[i + 1]),
low: Math.round(data.daily.temperature_2m_min[i + 1]),
code: data.daily.weather_code[i + 1],
})) : []);
} catch {
} finally {
setLoading(false);
}
}
async function getLocationByIP() {
try {
const res = await fetch("https://ipapi.co/json/");
const ipData = await res.json();
if (ipData.latitude && ipData.longitude) {
fetchWeather(ipData.latitude, ipData.longitude, ipData.city || "Unbekannt");
} else {
fetchWeather(defaultLat, defaultLon, defaultCity);
}
} catch {
fetchWeather(defaultLat, defaultLon, defaultCity);
}
}
getLocationByIP();
}, []);
const [forecast, setForecast] = useState<{ day: string; high: number; low: number; code: number }[]>([]);
if (loading) {
return (
<div className="bg-card rounded-lg border border-card-border p-4 aspect-square flex items-center justify-center" data-testid="sidebar-weather-loading">
<div className="w-full h-full bg-muted animate-pulse rounded" />
</div>
);
}
if (!weather) return null;
return (
<div className="bg-gradient-to-br from-card to-card/80 rounded-lg border border-card-border p-4 aspect-square flex flex-col justify-between" data-testid="sidebar-weather">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<MapPin className="w-3.5 h-3.5 text-primary" />
<span className="text-xs font-semibold text-card-foreground">{weather.city}</span>
</div>
<span className="text-[10px] text-muted-foreground">
{new Date().toLocaleDateString("de-DE", { weekday: "short", day: "numeric", month: "short" })}
</span>
</div>
<div className="flex items-center justify-center gap-3 flex-1">
<div className="scale-150">{getWeatherIcon(weather.weatherCode)}</div>
<div>
<div className="text-3xl font-bold text-card-foreground leading-none">{weather.temperature}°</div>
<div className="text-xs text-muted-foreground mt-0.5">{getWeatherText(weather.weatherCode)}</div>
</div>
</div>
<div className="flex items-center justify-between text-[10px] text-muted-foreground border-t border-card-border pt-2 mb-2">
<span className="flex items-center gap-1"><Wind className="w-3 h-3" />{weather.windSpeed} km/h</span>
<span className="flex items-center gap-1"><Droplets className="w-3 h-3" />{weather.humidity}%</span>
</div>
{forecast.length > 0 && (
<div className="flex gap-2 border-t border-card-border pt-2">
{forecast.map((f) => (
<div key={f.day} className="flex-1 text-center">
<div className="text-[10px] text-muted-foreground mb-1">{f.day}</div>
<div className="flex justify-center scale-75">{getWeatherIcon(f.code)}</div>
<div className="text-[10px] text-card-foreground font-medium">{f.high}°</div>
<div className="text-[10px] text-muted-foreground">{f.low}°</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -12,7 +12,7 @@ import { PhotoGalleryWidget } from "@/components/photo-gallery";
import { HoroscopeWidget } from "@/components/horoscope-widget";
import { RecipeWidget } from "@/components/recipe-widget";
import { NewsWidget } from "@/components/news-widget";
import { WeatherWidget } from "@/components/weather-widget";
import { WeatherWidget, SidebarWeatherWidget } from "@/components/weather-widget";
import { BreakingNewsWidget } from "@/components/breaking-news-widget";
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
@ -587,8 +587,9 @@ export default function Home() {
</div>
)}
</div>
<div className="lg:col-span-1">
<div className="lg:col-span-1 space-y-4">
{articles && articles.length > 0 && <TopStoriesList articles={[...articles].sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()).slice(0, 5)} />}
<SidebarWeatherWidget />
<SidebarAd />
</div>
</div>