Add weather information and an advertisement to the homepage
Introduce a new WeatherWidget component that fetches and displays local weather data using the Open-Meteo API and integrates an AdSense ad below it on the home page. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 01b9c0d3-17fd-4917-9326-1b5e3c5eb2ce 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
This commit is contained in:
parent
ae838f3222
commit
2c5feb8ea3
BIN
attached_assets/image_1772315386258.png
Normal file
BIN
attached_assets/image_1772315386258.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
134
client/src/components/weather-widget.tsx
Normal file
134
client/src/components/weather-widget.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
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}¤t=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 p-4 h-full" data-testid="widget-weather">
|
||||||
|
<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="flex items-center gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{getWeatherIcon(weather.weatherCode)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-2xl font-bold text-card-foreground">{weather.temperature}°C</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{getWeatherText(weather.weatherCode)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 mt-2 text-[10px] text-muted-foreground">
|
||||||
|
<MapPin className="w-3 h-3" />
|
||||||
|
<span>{weather.city}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 mt-2 text-[10px] text-muted-foreground">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ import { PhotoGalleryWidget } from "@/components/photo-gallery";
|
|||||||
import { HoroscopeWidget } from "@/components/horoscope-widget";
|
import { HoroscopeWidget } from "@/components/horoscope-widget";
|
||||||
import { RecipeWidget } from "@/components/recipe-widget";
|
import { RecipeWidget } from "@/components/recipe-widget";
|
||||||
import { NewsWidget } from "@/components/news-widget";
|
import { NewsWidget } from "@/components/news-widget";
|
||||||
|
import { WeatherWidget } from "@/components/weather-widget";
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
|
||||||
function useFocalPoints() {
|
function useFocalPoints() {
|
||||||
@ -408,6 +409,16 @@ export default function Home() {
|
|||||||
<PhotoGalleryWidget />
|
<PhotoGalleryWidget />
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<NewsWidget />
|
<NewsWidget />
|
||||||
|
<WeatherWidget />
|
||||||
|
<div className="bg-card rounded-lg border border-card-border overflow-hidden">
|
||||||
|
<AdSense
|
||||||
|
slot="auto"
|
||||||
|
format="fluid"
|
||||||
|
layoutKey="-6t+ed+2i-1n-4w"
|
||||||
|
style={{ display: "block" }}
|
||||||
|
className="w-full min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user