modified: app.py

modified:   static/css/style.css
	modified:   templates/base.html
	modified:   templates/index.html
	modified:   templates/weather.html
	new file:   translations.py
This commit is contained in:
simon
2026-04-27 11:06:16 +02:00
parent e793804cea
commit 2b671b10ff
6 changed files with 285 additions and 57 deletions

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="de">
<html lang="{{ lang }}">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@@ -15,17 +15,21 @@
<form class="nav-search" action="/wetter" method="get" autocomplete="off">
<div class="nav-search-wrap">
<svg class="nav-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="text" name="ort" id="nav-ort" placeholder="Ort suchen …" value="{{ request.args.get('ort','') }}"/>
<input type="text" name="ort" id="nav-ort" placeholder="{{ T.nav_placeholder }}" value="{{ request.args.get('ort','') }}"/>
<ul class="ac-list" id="nav-ac"></ul>
</div>
</form>
<span class="nav-clock" id="nav-clock"></span>
<span class="nav-clock" id="nav-clock" data-suffix="{{ T.clock_suffix }}"></span>
<nav class="lang-switch" aria-label="Language">
<a href="/set-lang?lang=de&next={{ request.full_path }}" class="lang-btn{% if lang == 'de' %} lang-btn--active{% endif %}" hreflang="de">DE</a>
<a href="/set-lang?lang=en&next={{ request.full_path }}" class="lang-btn{% if lang == 'en' %} lang-btn--active{% endif %}" hreflang="en">EN</a>
</nav>
</nav>
<main>{% block content %}{% endblock %}</main>
<footer class="footer">
Wetterdaten: <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst Open Data (MOSMIX)</a>
{{ T.data_source }} <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst Open Data (MOSMIX)</a>
</footer>
<script>
@@ -73,6 +77,7 @@ window.setupAC = setupAC;
const tz = tzMeta ? tzMeta.getAttribute("content") : null;
const navClock = document.getElementById("nav-clock");
const heroTime = document.getElementById("hero-time");
const suffix = navClock ? (navClock.dataset.suffix ? " " + navClock.dataset.suffix : "") : "";
function tick() {
const now = new Date();
@@ -83,8 +88,8 @@ window.setupAC = setupAC;
} catch (e) {
timeStr = new Intl.DateTimeFormat("de-DE", opts).format(now);
}
if (navClock) navClock.textContent = timeStr + " Uhr";
if (heroTime) heroTime.textContent = timeStr + " Uhr";
if (navClock) navClock.textContent = timeStr + suffix;
if (heroTime) heroTime.textContent = timeStr + suffix;
}
tick();

View File

@@ -1,24 +1,24 @@
{% extends "base.html" %}
{% block title %}Skywatcher Wetter für Deutschland{% endblock %}
{% block title %}{{ T.page_title_home }}{% endblock %}
{% block content %}
<div class="home-wrap">
<div class="home-bg-text" aria-hidden="true">WETTER</div>
<div class="home-center">
<p class="home-eyebrow">Präzise Vorhersagen · Daten via DWD Open Data</p>
<h1 class="home-title">Wo willst du<br>das Wetter wissen?</h1>
<p class="home-eyebrow">{{ T.home_eyebrow }}</p>
<h1 class="home-title">{{ T.home_headline|safe }}</h1>
<form class="home-form" action="/wetter" method="get" autocomplete="off">
<div class="home-input-wrap">
<svg class="home-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
<input type="text" name="ort" id="home-ort" placeholder="Stadt, Gemeinde oder PLZ …" required autofocus/>
<input type="text" name="ort" id="home-ort" placeholder="{{ T.home_placeholder }}" required autofocus/>
<ul class="ac-list" id="home-ac"></ul>
</div>
<button type="submit" class="home-btn">
Abrufen
{{ T.home_btn }}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</form>
@@ -30,9 +30,9 @@
<a href="/wetter?ort={{ city }}" class="chip">{{ city }}</a>
{% endfor %}
</div>
<button class="btn-location" id="btn-location" title="Meinen Standort nutzen">
<button class="btn-location" id="btn-location" title="{{ T.btn_location }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/><circle cx="12" cy="12" r="9" opacity=".3"/></svg>
Meinen Standort nutzen
{{ T.btn_location }}
</button>
</div>
@@ -49,14 +49,20 @@
{% block scripts %}
<script>
const _i18n = {
locating: {{ T.locating | tojson }},
locationUnavailable: {{ T.location_unavailable | tojson }},
geoUnsupported: {{ T.geolocation_unsupported | tojson }},
recentlySearched: {{ T.recently_searched | tojson }},
};
// Standort-Button
document.getElementById("btn-location").addEventListener("click", function() {
if (!navigator.geolocation) { alert("Geolocation nicht unterstützt."); return; }
this.textContent = "Wird ermittelt …";
if (!navigator.geolocation) { alert(_i18n.geoUnsupported); return; }
this.textContent = _i18n.locating;
this.disabled = true;
navigator.geolocation.getCurrentPosition(
pos => { window.location = `/wetter?lat=${pos.coords.latitude}&lon=${pos.coords.longitude}&ort=Mein+Standort`; },
() => { this.textContent = "Standort nicht verfügbar"; this.disabled = false; }
() => { this.textContent = _i18n.locationUnavailable; this.disabled = false; }
);
});
@@ -67,7 +73,7 @@ document.getElementById("btn-location").addEventListener("click", function() {
const wrap = document.getElementById("home-chips");
const div = document.createElement("div");
div.className = "recent-label";
div.textContent = "Zuletzt gesucht:";
div.textContent = _i18n.recentlySearched;
wrap.before(div);
const strip = document.createElement("div");
strip.className = "home-chips home-chips--recent";

View File

@@ -23,7 +23,7 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
{{ display_name.split(',')[0] }}
<span class="hero-meta-sub">{{ display_name.split(',')[1:3]|join(',') if ',' in display_name else '' }}</span>
<span class="hero-meta-time" id="hero-time">{{ now_local }} Uhr</span>
<span class="hero-meta-time" id="hero-time">{{ now_local }}{% if T.clock_suffix %} {{ T.clock_suffix }}{% endif %}</span>
</div>
<div class="hero-main">
@@ -39,7 +39,7 @@
<span>{{ current.wind_kmh }} km/h {{ wind_dir_name(current.wind_dir) }}</span>
{% endif %}
{% if current.cloud_pct is not none %}
<span>{{ current.cloud_pct }}% Bedeckung</span>
<span>{{ current.cloud_pct }}% {{ T.cloud_cover }}</span>
{% endif %}
{% if current.pressure_hpa is not none %}
<span>{{ current.pressure_hpa }} hPa</span>
@@ -56,11 +56,11 @@
{% set uv_curr_str = "" %}
{% endif %}
{% set items = [
("Gefühlt wie", (current.feels_like|string + " °C") if current.feels_like is not none else ""),
("Böen", (current.gust_kmh|string + " km/h") if current.gust_kmh is not none else ""),
("Niederschlag", (current.precip_mm|string + " mm") if (current.precip_mm is not none and current.precip_mm > 0) else ((current.rain_prob|string + " %") if (current.rain_prob is not none and current.rain_prob > 0) else "0 mm")),
("Sonne", (current.sun_min|string + " min/h") if (current.sun_min is not none and current.sun_min > 0) else ""),
("UV-Index", uv_curr_str),
(T.feels_like, (current.feels_like|string + " °C") if current.feels_like is not none else ""),
(T.gusts, (current.gust_kmh|string + " km/h") if current.gust_kmh is not none else ""),
(T.precipitation, (current.precip_mm|string + " mm") if (current.precip_mm is not none and current.precip_mm > 0) else ((current.rain_prob|string + " %") if (current.rain_prob is not none and current.rain_prob > 0) else "0 mm")),
(T.sun, (current.sun_min|string + " min/h") if (current.sun_min is not none and current.sun_min > 0) else ""),
(T.uv_index, uv_curr_str),
] %}
{% for label, val in items %}
<div class="hero-metric">
@@ -72,7 +72,7 @@
</div>
<div class="hero-station">
📡 Station <strong>{{ station_name }}</strong> ({{ station_dist }} km)
📡 {{ T.station }} <strong>{{ station_name }}</strong> ({{ station_dist }} km)
{% if sunrise %}
&nbsp;·&nbsp; 🌅 {{ sunrise }} &nbsp;·&nbsp; 🌇 {{ sunset }}
{% endif %}
@@ -101,7 +101,7 @@
<p class="warn-desc">{{ short_desc }}{% if short_desc|length < w.description|length %}.{% endif %}</p>
{% if short_desc|length < w.description|length %}
<details class="warn-more">
<summary>Details</summary>
<summary>{{ T.warn_details }}</summary>
<p>{{ w.description }}</p>
</details>
{% endif %}
@@ -113,33 +113,33 @@
{% endif %}
<section class="section insights">
<h2 class="section-title">Kurzfazit</h2>
<h2 class="section-title">{{ T.section_summary }}</h2>
<div class="insight-grid">
<div class="insight-card">
<span class="insight-label">Temperaturtrend (6h)</span>
<span class="insight-label">{{ T.temp_trend_label }}</span>
<strong class="insight-value">
{% if temp_delta_6h is not none %}
{% if temp_delta_6h > 0 %}+{% endif %}{{ temp_delta_6h }}° · {{ temp_trend_6h }}
{% if temp_delta_6h > 0 %}+{% endif %}{{ temp_delta_6h }}° · {{ T.get(temp_trend_6h, temp_trend_6h) }}
{% else %}
{% endif %}
</strong>
</div>
<div class="insight-card">
<span class="insight-label">Drucktrend (6h)</span>
<span class="insight-label">{{ T.pressure_trend_label }}</span>
<strong class="insight-value">
{% if pressure_delta is not none %}
{% if pressure_delta > 0 %}+{% endif %}{{ pressure_delta }} hPa · {{ pressure_trend }}
{% if pressure_delta > 0 %}+{% endif %}{{ pressure_delta }} hPa · {{ T.get(pressure_trend, pressure_trend) }}
{% else %}
{% endif %}
</strong>
</div>
<div class="insight-card">
<span class="insight-label">Bestes Aktivitätsfenster</span>
<span class="insight-label">{{ T.best_window_label }}</span>
<strong class="insight-value">
{% if best_window %}
{{ best_window.start.strftime('%H:%M') }}{{ best_window.end.strftime('%H:%M') }} · Score {{ best_window.score }}
{{ best_window.start.strftime('%H:%M') }}{{ best_window.end.strftime('%H:%M') }} · {{ T.score }} {{ best_window.score }}
{% else %}
{% endif %}
@@ -150,23 +150,23 @@
<!-- STÜNDLICH -->
<section class="section">
<h2 class="section-title">Stundenweise</h2>
<h2 class="section-title">{{ T.section_hourly }}</h2>
<div class="hourly-legend">
<span class="hl-label">Legende:</span>
<span class="hl-badge hl-conf hl-conf--hoch">Konfidenz hoch</span>
<span class="hl-badge hl-conf hl-conf--mittel">mittel</span>
<span class="hl-badge hl-conf hl-conf--niedrig">niedrig</span>
<span class="hl-label">{{ T.legend }}</span>
<span class="hl-badge hl-conf hl-conf--hoch">{{ T.conf_high }}</span>
<span class="hl-badge hl-conf hl-conf--mittel">{{ T.conf_mid }}</span>
<span class="hl-badge hl-conf hl-conf--niedrig">{{ T.conf_low }}</span>
<span class="hl-sep">·</span>
<span class="hl-badge hl-act">Aktivität 0100</span>
<span class="hl-badge hl-act">{{ T.activity_legend }}</span>
<span class="hl-sep">·</span>
<span class="hl-badge hl-uv">UV-Index</span>
<span class="hl-badge hl-uv">{{ T.uv_index }}</span>
</div>
<div class="hourly-strip-wrap">
<div class="hourly-strip">
{% for h in forecast %}
<div class="hcard {% if loop.first %}hcard--now{% endif %}">
<div class="hcard-time">
{% if loop.first %}Jetzt
{% if loop.first %}{{ T.now }}
{% elif h.datetime is string %}{{ h.datetime[11:16] }}
{% else %}{{ h.datetime.strftime('%H:%M') }}{% endif %}
</div>
@@ -193,9 +193,9 @@
<div class="hcard-wind">{{ h.wind_kmh }}<small>km/h</small></div>
{% endif %}
<div class="hcard-confidence hcard-confidence--{{ h.confidence_label }}">{{ h.confidence }}%</div>
<div class="hcard-activity">Aktiv {{ h.activity_score }}</div>
<div class="hcard-activity">{{ T.active }} {{ h.activity_score }}</div>
{% if h.uv_index is not none and h.uv_index > 0 %}
<div class="hcard-uv hcard-uv--{{ h.uv_level }}">UV {{ h.uv_index }} {{ h.uv_label }}</div>
<div class="hcard-uv hcard-uv--{{ h.uv_level }}">UV {{ h.uv_index }} {{ T.get(h.uv_label, h.uv_label) }}</div>
{% endif %}
</div>
{% endfor %}
@@ -206,8 +206,8 @@
<!-- CHART -->
<section class="section">
<div class="chart-head">
<h2 class="section-title">Temperatur, Gefühlt &amp; Niederschlag</h2>
<span class="chart-range">72 Stunden</span>
<h2 class="section-title">{{ T.section_chart }}</h2>
<span class="chart-range">{{ T.chart_range }}</span>
</div>
<div class="chart-box">
<canvas id="wxChart"></canvas>
@@ -216,11 +216,11 @@
<!-- TAGESÜBERSICHT -->
<section class="section">
<h2 class="section-title">Tagesübersicht</h2>
<h2 class="section-title">{{ T.section_daily }}</h2>
<div class="daily-header">
<span>Tag</span>
<span>Temperaturbereich</span>
<span>Min &nbsp;·&nbsp; Max &nbsp;·&nbsp; Nieder. &nbsp;·&nbsp; UV</span>
<span>{{ T.daily_hdr_day }}</span>
<span>{{ T.daily_hdr_range }}</span>
<span>{{ T.daily_hdr_details }}</span>
</div>
<div class="daily-list">
{# calc global min/max for bar scaling do it in a loop to stay Jinja2-safe #}
@@ -282,7 +282,7 @@
</section>
<p class="data-note">
Wetterdaten: <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst Open Data (MOSMIX)</a>
{{ T.data_source }} <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst Open Data (MOSMIX)</a>
</p>
{% endblock %}
@@ -308,7 +308,7 @@
// Prüfen ob echte Niederschlagsmengen vorhanden sind
const hasRealPrecip = precip.some(v => v > 0);
const barData = hasRealPrecip ? precip : rainProb;
const barLabel = hasRealPrecip ? "Niederschlag (mm)" : "Regenwahrsch. (%)";
const barLabel = hasRealPrecip ? {{ T.chart_precip | tojson }} : {{ T.chart_rainprob | tojson }};
const barMax = hasRealPrecip ? undefined : 100;
// Nur jeden 3. Label anzeigen, Rest leer lassen
@@ -327,7 +327,7 @@
datasets: [
{
type: "line",
label: "Temperatur (°C)",
label: {{ T.chart_temp | tojson }},
data: temps,
borderColor: "#ff8c32",
backgroundColor: grad,
@@ -341,7 +341,7 @@
},
{
type: "line",
label: "Gefühlt (°C)",
label: {{ T.chart_feels | tojson }},
data: feels,
borderColor: "#ffd4a8",
borderDash: [5, 4],