All checks were successful
secrets-guard / encrypted (push) Successful in 18s
shell-lint / shellcheck (push) Successful in 17s
Build + push images (Forgejo registry) / build-push (frontend) (push) Successful in 1m17s
Build + push images (Forgejo registry) / build-push (backend) (push) Successful in 1m23s
Deploy / deploy (backend) (push) Successful in 1m40s
Deploy / deploy (frontend) (push) Successful in 1m48s
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 54s
PR build (required check) / build-backend (pull_request) Successful in 1m11s
PR build (required check) / gate (pull_request) Successful in 1s
209 lines
10 KiB
Django/Jinja
209 lines
10 KiB
Django/Jinja
{% extends "base.html.j2" %}
|
|
{# The Weekly view — the product itself — with the distribution surfaces built
|
|
around it. Everything structural is server-rendered so crawlers and no-JS
|
|
readers get a complete page; app.js hydrates the live numbers in place.
|
|
|
|
The interactive controls below (#find-btn, #date-input, #panel, #results, …)
|
|
are app.js's DOM contract, carried over verbatim from the old static
|
|
frontend/index.html. Do not rename these ids.
|
|
|
|
section / brand_tag / main_class come from the route's render context
|
|
(content.home_page), not a child-template {% set %} — the SEO pages already
|
|
pass `section` that way. #}
|
|
|
|
{% block title %}Thermograph: how unusual is your weather?{% endblock %}
|
|
{% block description %}See how today's weather compares to decades of local climate history for any place on Earth. Daily high, low, feels-like, humidity, wind and rain, each graded by percentile.{% endblock %}
|
|
{% block canonical_path %}/{% endblock %}
|
|
|
|
{% block head_extra %}
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
{% endblock %}
|
|
|
|
{% block jsonld %}
|
|
<script type="application/ld+json">
|
|
{{ jsonld_str }}
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{# --- Hero ------------------------------------------------------------
|
|
The grade card is the LCP element: its frame is server-rendered and its
|
|
slots are sized here, so hydrating the live numbers causes no layout
|
|
shift. Band colour is always paired with the band word — colour is never
|
|
the only signal. #}
|
|
<section id="hero" class="hero">
|
|
<div class="hero-copy">
|
|
<h1>How unusual is your weather?</h1>
|
|
<p class="hero-sub">Any day, anywhere on Earth, graded against 45 years of that place's own history.</p>
|
|
</div>
|
|
|
|
<div class="hero-card">
|
|
{# --cat carries the band colour, the same convention .normal-card uses,
|
|
so the band token stays the single source of truth. #}
|
|
<div class="grade-card" id="hero-grade" aria-live="polite"
|
|
{% if unusual %}style="--cat: var(--{{ unusual.cls }})"{% endif %}>
|
|
<p class="grade-where" id="hero-where">
|
|
{%- if unusual -%}
|
|
Today in {{ unusual.display }}
|
|
{%- else -%}
|
|
Today
|
|
{%- endif -%}
|
|
</p>
|
|
<p class="grade-band" id="hero-band">{{ unusual.grade if unusual else '—' }}</p>
|
|
<p class="grade-detail" id="hero-detail">
|
|
{%- if unusual -%}
|
|
{{ unusual.metric_label }} in the {{ unusual.percentile|ordinal }} percentile for {{ unusual.window_label }}
|
|
{%- else -%}
|
|
Pick a place to see how unusual its weather is today.
|
|
{%- endif -%}
|
|
</p>
|
|
{% if unusual and unusual.is_default %}
|
|
<p class="grade-why muted">the most unusual weather we're tracking{% if stale %} · as of {{ unusual.date }}{% endif %}</p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="hero-actions">
|
|
{# app.js hides the locate button outside a secure context, where the
|
|
browser refuses geolocation and it could only ever fail. #}
|
|
<button type="button" id="hero-locate" class="btn-primary">Use my location</button>
|
|
<button type="button" id="hero-pick" class="btn-ghost">Pick on the map</button>
|
|
</div>
|
|
<p class="hero-locate-msg" id="hero-locate-msg" role="status" aria-live="polite" hidden></p>
|
|
<p class="hero-jump"><a href="#panel">See your week ↓</a></p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="controls">
|
|
<div class="find-bar">
|
|
<button type="button" id="find-btn" class="find-btn"></button>
|
|
<span id="loc-label" class="loc-label" hidden></span>
|
|
</div>
|
|
<div class="date-row">
|
|
<label>Target date
|
|
<input id="date-input" type="date" />
|
|
</label>
|
|
<button type="button" id="today-btn" class="today-chip" title="Reset the target date to today">Today</button>
|
|
<span class="hint"><a href="{{ asset_base }}/legend">What do the grades mean? →</a></span>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="panel" class="panel panel-full">
|
|
<div class="placeholder" id="placeholder">
|
|
<p>Find a location to begin.</p>
|
|
<p class="muted">Thermograph fetches decades of daily highs, lows, and precipitation
|
|
for your ~4 sq mi cell, builds a ±7-day seasonal distribution for each
|
|
day of the year, and grades recent weather by where it falls on that distribution.</p>
|
|
</div>
|
|
<div id="results" hidden></div>
|
|
</section>
|
|
|
|
{# Sits outside #panel on purpose: app.js rewrites results.innerHTML
|
|
wholesale on every grade, so anything inside it would be destroyed. #}
|
|
<p class="stance-line muted">
|
|
Free. No ads. No tracking. No account needed.
|
|
<a href="{{ base }}/about">How we work →</a>
|
|
</p>
|
|
|
|
{# --- Unusual right now ----------------------------------------------
|
|
Scroll-snap row, no JS carousel. Both tails are represented whenever a
|
|
cold-tail city qualifies — two-sided honesty is product identity. #}
|
|
{% if ranked %}
|
|
<section class="unusual-strip" aria-labelledby="records-title">
|
|
<h2 id="records-title">Unusual right now</h2>
|
|
<ul class="unusual-row">
|
|
{% for r in ranked %}
|
|
<li class="unusual-card" style="--cat: var(--{{ r.cls }})">
|
|
<a href="{{ asset_base }}/day#lat={{ r.lat }}&lon={{ r.lon }}"
|
|
data-event="home.records_click">
|
|
<span class="unusual-city">{{ r.display }}</span>
|
|
{# The reading itself, then what it is and what it should be —
|
|
a percentile means little without the value it describes. #}
|
|
<span class="unusual-value">{{ temp(r.value) }}</span>
|
|
<span class="unusual-band">{{ r.grade_label }}</span>
|
|
<span class="unusual-meta">
|
|
{{ r.metric_label }} · {{ r.pct_label }} pct
|
|
{%- if r.normal is not none %} · normal {{ temp(r.normal) }}{% endif %}
|
|
{%- if r.date_label %} · {{ r.date_label }}{% endif %}
|
|
</span>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<p class="unusual-more"><a href="{{ base }}/climate">See all records →</a></p>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# --- How it works ---------------------------------------------------- #}
|
|
<section class="how-it-works" aria-labelledby="how-title">
|
|
<h2 id="how-title">How it works</h2>
|
|
<ol class="how-steps">
|
|
<li>We keep 45 years of climate history (ERA5 reanalysis) for your exact ~2-mile grid square.</li>
|
|
<li>Today gets compared to the same two weeks of the year, every year back to 1980.</li>
|
|
<li>The result is a percentile and a plain-language grade, from Near Record cold to Near Record hot.</li>
|
|
</ol>
|
|
<p class="muted">
|
|
<a href="{{ asset_base }}/legend">Full methodology →</a>
|
|
· Data: ERA5 via Open-Meteo · NASA POWER · MET Norway
|
|
</p>
|
|
</section>
|
|
|
|
{# --- Explore --------------------------------------------------------- #}
|
|
<section class="explore" aria-labelledby="explore-title">
|
|
<h2 id="explore-title">Explore</h2>
|
|
{# Each card takes a colour from the grade ramp rather than a decorative
|
|
palette, so the section reads as part of the product's own scale.
|
|
Calendar carries the ramp itself — it IS the heatmap. #}
|
|
<div class="explore-cards">
|
|
<a class="explore-card" href="{{ asset_base }}/calendar" data-event="home.nav_calendar"
|
|
style="--cat: var(--warm)">
|
|
<span class="explore-name">Calendar</span>
|
|
<span class="explore-ramp" aria-hidden="true">
|
|
{% for t in ('rec-cold','very-cold','cold','cool','normal','warm','hot','very-hot','rec-hot') %}
|
|
<i style="background: var(--{{ t }})"></i>
|
|
{% endfor %}
|
|
</span>
|
|
<span class="explore-desc">Two years of your days, each colored by how unusual it was.</span>
|
|
</a>
|
|
<a class="explore-card" href="{{ asset_base }}/compare" data-event="home.nav_compare"
|
|
style="--cat: var(--normal)">
|
|
<span class="explore-name">Compare</span>
|
|
<span class="explore-desc">Rank any places by comfort for a trip or a move.</span>
|
|
</a>
|
|
<a class="explore-card" href="{{ asset_base }}/day" data-event="home.nav_day"
|
|
style="--cat: var(--cool)">
|
|
<span class="explore-name">Day</span>
|
|
<span class="explore-desc">Any date since 1980, anywhere: what was normal, what happened.</span>
|
|
</a>
|
|
<a class="explore-card" href="{{ asset_base }}/alerts" data-event="home.nav_alerts"
|
|
style="--cat: var(--rec-hot)">
|
|
<span class="explore-name">Alerts</span>
|
|
<span class="explore-desc">Free percentile alerts: get pinged when your weather goes statistically weird.</span>
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
{# --- City hub links --------------------------------------------------
|
|
Real HTML links funnelling homepage authority into the ~1000-page
|
|
/climate surface. #}
|
|
<section class="city-hub" aria-labelledby="cities-title">
|
|
<h2 id="cities-title">Climate context for 1,000 cities</h2>
|
|
<ul class="city-chips">
|
|
{% for c in cities %}
|
|
<li><a href="{{ base }}/climate/{{ c.slug }}" data-event="home.nav_city">{{ c.name }}</a></li>
|
|
{% endfor %}
|
|
</ul>
|
|
<p><a href="{{ base }}/climate" data-event="home.nav_cities">Browse all cities →</a></p>
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block body_scripts %}
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script type="module" src="{{ asset_base }}/app.js"></script>
|
|
{# Bookmarked locations: a star toggle in the results share row + a "Saved"
|
|
dropdown by the Find button. Loaded as its own module alongside app.js
|
|
(not merged into it) — see bookmarks-ui.js's header comment for why. #}
|
|
<script type="module" src="{{ asset_base }}/bookmarks-ui.js"></script>
|
|
{# The records strip renders real temperatures server-side as .temp spans, so
|
|
it needs the same converter the SEO pages use or they'd stay in °F while
|
|
the toggle says °C. Both import units.js, which the module loader dedupes. #}
|
|
<script type="module" src="{{ asset_base }}/climate.js"></script>
|
|
{% endblock %}
|