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:
parent
e0a9faf61c
commit
1fecc39035
@ -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}¤t=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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user