feat: Wetter-Widget responsive über volle Breite im Desktop-Dashboard
Auf Desktop wird das Wetter-Widget über allen anderen Widgets platziert mit horizontalem Layout (aktuelles Wetter links, Vorhersage rechts). Vorhersagezeitraum skaliert mit Bildschirmbreite: 3 Tage (Mobil), 4 Tage (Tablet), 5 Tage (Desktop/Wide). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-12
@@ -269,11 +269,12 @@ function renderWeatherWidget(weather) {
|
||||
|
||||
const { city, current, forecast } = weather;
|
||||
|
||||
const forecastHtml = forecast.map((d) => {
|
||||
const forecastHtml = forecast.map((d, i) => {
|
||||
const date = new Date(d.date + 'T12:00:00');
|
||||
const label = date.toLocaleDateString('de-DE', { weekday: 'short' });
|
||||
const extraCls = i >= 3 ? ' weather-forecast__day--extended' : '';
|
||||
return `
|
||||
<div class="weather-forecast__day">
|
||||
<div class="weather-forecast__day${extraCls}">
|
||||
<div class="weather-forecast__label">${label}</div>
|
||||
<img class="weather-forecast__icon" src="${WEATHER_ICON_BASE}${d.icon}@2x.png"
|
||||
alt="${d.desc}" width="32" height="32" loading="lazy">
|
||||
@@ -286,19 +287,21 @@ function renderWeatherWidget(weather) {
|
||||
|
||||
return `
|
||||
<div class="widget weather-widget">
|
||||
<div class="weather-widget__main">
|
||||
<div class="weather-widget__left">
|
||||
<div class="weather-widget__temp">${current.temp}°C</div>
|
||||
<div class="weather-widget__desc">${current.desc}</div>
|
||||
<div class="weather-widget__city">${city}</div>
|
||||
<div class="weather-widget__meta">
|
||||
Gefühlt ${current.feels_like}° · ${current.humidity}% · Wind ${current.wind_speed} km/h
|
||||
<div class="weather-widget__inner">
|
||||
<div class="weather-widget__main">
|
||||
<div class="weather-widget__left">
|
||||
<div class="weather-widget__temp">${current.temp}°C</div>
|
||||
<div class="weather-widget__desc">${current.desc}</div>
|
||||
<div class="weather-widget__city">${city}</div>
|
||||
<div class="weather-widget__meta">
|
||||
Gefühlt ${current.feels_like}° · ${current.humidity}% · Wind ${current.wind_speed} km/h
|
||||
</div>
|
||||
</div>
|
||||
<img class="weather-widget__icon" src="${WEATHER_ICON_BASE}${current.icon}@2x.png"
|
||||
alt="${current.desc}" width="80" height="80" loading="lazy">
|
||||
</div>
|
||||
<img class="weather-widget__icon" src="${WEATHER_ICON_BASE}${current.icon}@2x.png"
|
||||
alt="${current.desc}" width="80" height="80" loading="lazy">
|
||||
${forecast.length ? `<div class="weather-forecast">${forecastHtml}</div>` : ''}
|
||||
</div>
|
||||
${forecast.length ? `<div class="weather-forecast">${forecastHtml}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -507,7 +507,9 @@
|
||||
* Wetter-Widget
|
||||
*
|
||||
* Eigener Gradient, überschreibt .widget Hintergrund.
|
||||
* Kompakt: Icon kleiner, Forecast enger.
|
||||
* Mobile: kompakt, 3-Tage-Vorhersage.
|
||||
* Desktop: volle Breite über allen Widgets, horizontales
|
||||
* Layout mit bis zu 5-Tage-Vorhersage.
|
||||
* -------------------------------------------------------- */
|
||||
.weather-widget {
|
||||
background: linear-gradient(135deg, var(--color-accent) 0%, #1E5CB3 100%);
|
||||
@@ -569,6 +571,11 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Tage 4+5 nur auf breiteren Bildschirmen sichtbar */
|
||||
.weather-forecast__day--extended {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.weather-forecast__label {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
@@ -595,6 +602,97 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Tablet: Wetter über volle Breite, 4 Tage */
|
||||
@media (min-width: 768px) {
|
||||
.weather-widget {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.weather-widget__inner {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.weather-widget__main {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.weather-forecast {
|
||||
border-top: none;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.15);
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
.weather-forecast__day--extended:nth-child(-n+4) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop: 5-Tage-Vorhersage, größere Elemente */
|
||||
@media (min-width: 1024px) {
|
||||
.weather-widget {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.weather-widget__main {
|
||||
padding: var(--space-4) var(--space-5);
|
||||
}
|
||||
|
||||
.weather-widget__temp {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.weather-widget__icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.weather-forecast {
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-5);
|
||||
}
|
||||
|
||||
.weather-forecast__day--extended {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.weather-forecast__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.weather-forecast__temps {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.weather-forecast__label {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wide Desktop: noch mehr Platz */
|
||||
@media (min-width: 1440px) {
|
||||
.weather-widget {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.weather-widget__main {
|
||||
padding: var(--space-4) var(--space-6);
|
||||
}
|
||||
|
||||
.weather-forecast {
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-3) var(--space-6);
|
||||
}
|
||||
|
||||
.weather-forecast__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Skeleton-Zustände (pro Widget)
|
||||
* Kompakt: gleiches Spacing wie echte Widgets.
|
||||
|
||||
@@ -15,7 +15,7 @@ const CACHE_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
// --------------------------------------------------------
|
||||
// GET /api/v1/weather
|
||||
// Gibt aktuelles Wetter + 3-Tage-Vorschau zurück.
|
||||
// Gibt aktuelles Wetter + 5-Tage-Vorschau zurück.
|
||||
// Erfordert OPENWEATHER_API_KEY + OPENWEATHER_CITY in .env
|
||||
// Response: { data: { current, forecast } } | { data: null }
|
||||
// --------------------------------------------------------
|
||||
@@ -49,7 +49,7 @@ router.get('/', async (req, res) => {
|
||||
const currentJson = await currentRes.json();
|
||||
|
||||
// 5-Tage-Forecast (3h-Intervalle → wir nehmen Mittags-Werte für Tagesvorschau)
|
||||
const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?q=${encodeURIComponent(city)}&appid=${apiKey}&units=${units}&lang=${lang}&cnt=24`;
|
||||
const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?q=${encodeURIComponent(city)}&appid=${apiKey}&units=${units}&lang=${lang}&cnt=40`;
|
||||
const forecastRes = await fetch(forecastUrl, { signal: AbortSignal.timeout(8000) });
|
||||
let forecastDays = [];
|
||||
if (forecastRes.ok) {
|
||||
@@ -67,7 +67,7 @@ router.get('/', async (req, res) => {
|
||||
icon: item.weather[0]?.icon,
|
||||
desc: item.weather[0]?.description,
|
||||
});
|
||||
if (forecastDays.length >= 3) break;
|
||||
if (forecastDays.length >= 5) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user