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:
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 %}
|
||||
· 🌅 {{ sunrise }} · 🌇 {{ 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 0–100</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 & 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 · Max · Nieder. · 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],
|
||||
|
||||
Reference in New Issue
Block a user