feat(ui): add empty states for stats and data sections

Show blurred placeholder cards with "No stats yet" / "No data yet"
labels when no data exists, instead of hiding the sections entirely.
This commit is contained in:
Jon Seager 2026-02-08 16:10:32 +00:00
parent ffee36ea41
commit f5e319ce03
No known key found for this signature in database
3 changed files with 95 additions and 2 deletions

View file

@ -117,6 +117,10 @@ pub(crate) async fn stats_page(
.collect(); .collect();
let max_grinder_weight = cached.brewing_summary.max_grinder_weight; let max_grinder_weight = cached.brewing_summary.max_grinder_weight;
let has_data = cached.consumption.brews_all_time > 0
|| cached.roast_summary.unique_origins > 0
|| !cached.brewing_summary.brewer_counts.is_empty();
let template = StatsPageTemplate { let template = StatsPageTemplate {
nav_active: "stats", nav_active: "stats",
is_authenticated, is_authenticated,
@ -137,6 +141,7 @@ pub(crate) async fn stats_page(
consumption_30d_weight, consumption_30d_weight,
consumption_all_time_weight, consumption_all_time_weight,
cache_age, cache_age,
has_data,
}; };
render_html(template).map(IntoResponse::into_response) render_html(template).map(IntoResponse::into_response)

View file

@ -258,7 +258,6 @@ is_authenticated %}
</section> </section>
<!-- Stats --> <!-- Stats -->
{% if !stat_cards.is_empty() %}
<section> <section>
<div class="flex items-center justify-between mb-5"> <div class="flex items-center justify-between mb-5">
<h2 class="text-lg font-semibold text-text">Stats</h2> <h2 class="text-lg font-semibold text-text">Stats</h2>
@ -266,6 +265,7 @@ is_authenticated %}
>View all stats &rarr;</a >View all stats &rarr;</a
> >
</div> </div>
{% if !stat_cards.is_empty() %}
<chip-scroll class="relative block"> <chip-scroll class="relative block">
<button type="button" aria-label="Scroll left" <button type="button" aria-label="Scroll left"
class="hidden absolute -left-4 top-1/2 z-10 -translate-y-1/2 items-center justify-center rounded-full border bg-surface p-1.5 text-text-muted shadow-sm transition hover:text-text" class="hidden absolute -left-4 top-1/2 z-10 -translate-y-1/2 items-center justify-center rounded-full border bg-surface p-1.5 text-text-muted shadow-sm transition hover:text-text"
@ -289,8 +289,23 @@ is_authenticated %}
{{ icons::chevron_right("h-5 w-5") }} {{ icons::chevron_right("h-5 w-5") }}
</button> </button>
</chip-scroll> </chip-scroll>
{% else %}
<div class="relative">
<div class="grid grid-flow-col auto-cols-[minmax(45vw,max-content)] md:auto-cols-[minmax(200px,max-content)] gap-3 overflow-hidden blur-[2px] select-none pointer-events-none" aria-hidden="true">
{% for _ in 0..6 %}
<div class="flex flex-col items-center gap-1 rounded-lg border p-4">
<div class="h-6 w-6 rounded-full bg-surface-alt"></div>
<div class="h-5 w-16 rounded bg-surface-alt"></div>
<div class="h-4 w-20 rounded bg-surface-alt"></div>
</div>
{% endfor %}
</div>
<div class="absolute inset-0 z-10 flex items-center justify-center">
<span class="text-lg font-semibold text-text-muted">No stats yet</span>
</div>
</div>
{% endif %}
</section> </section>
{% endif %}
<!-- Data Cards --> <!-- Data Cards -->
<section> <section>
@ -300,6 +315,7 @@ is_authenticated %}
>View all data &rarr;</a >View all data &rarr;</a
> >
</div> </div>
{% if !stats.is_empty() %}
<chip-scroll class="relative block"> <chip-scroll class="relative block">
<button type="button" aria-label="Scroll left" <button type="button" aria-label="Scroll left"
class="hidden absolute -left-4 top-1/2 z-10 -translate-y-1/2 items-center justify-center rounded-full border bg-surface p-1.5 text-text-muted shadow-sm transition hover:text-text" class="hidden absolute -left-4 top-1/2 z-10 -translate-y-1/2 items-center justify-center rounded-full border bg-surface p-1.5 text-text-muted shadow-sm transition hover:text-text"
@ -364,5 +380,21 @@ is_authenticated %}
{{ icons::chevron_right("h-5 w-5") }} {{ icons::chevron_right("h-5 w-5") }}
</button> </button>
</chip-scroll> </chip-scroll>
{% else %}
<div class="relative">
<div class="grid grid-flow-col auto-cols-[45vw] md:auto-cols-[200px] gap-3 overflow-hidden blur-[2px] select-none pointer-events-none" aria-hidden="true">
{% for _ in 0..6 %}
<div class="flex flex-col items-center gap-1 rounded-lg border p-4">
<div class="h-6 w-6 rounded-full bg-surface-alt"></div>
<div class="h-5 w-8 rounded bg-surface-alt"></div>
<div class="h-4 w-16 rounded bg-surface-alt"></div>
</div>
{% endfor %}
</div>
<div class="absolute inset-0 z-10 flex items-center justify-center">
<span class="text-lg font-semibold text-text-muted">No data yet</span>
</div>
</div>
{% endif %}
</section> </section>
{% endblock %} {% endblock %}

View file

@ -10,6 +10,7 @@
<p class="max-w-2xl text-sm text-text-secondary">Aggregated coffee data across origins, consumption, and brewing.</p> <p class="max-w-2xl text-sm text-text-secondary">Aggregated coffee data across origins, consumption, and brewing.</p>
</header> </header>
{% if has_data %}
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
{% include "partials/tab_bar.html" %} {% include "partials/tab_bar.html" %}
@ -120,4 +121,59 @@
{% endif %} {% endif %}
</div> </div>
</section> </section>
{% else %}
<div class="relative">
<div class="flex flex-col gap-8 blur-[2px] select-none pointer-events-none" aria-hidden="true">
<!-- Placeholder: Roast Stats -->
<section>
<div class="mb-5"><div class="h-5 w-28 rounded bg-surface-alt"></div></div>
<div class="grid gap-3 sm:grid-cols-3">
{% for _ in 0..3 %}
<div class="flex flex-col items-center gap-1 rounded-lg border p-4">
<div class="h-6 w-6 rounded-full bg-surface-alt"></div>
<div class="h-5 w-10 rounded bg-surface-alt"></div>
<div class="h-4 w-16 rounded bg-surface-alt"></div>
</div>
{% endfor %}
</div>
</section>
<!-- Placeholder: Consumption -->
<section>
<div class="mb-5"><div class="h-5 w-32 rounded bg-surface-alt"></div></div>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
{% for _ in 0..4 %}
<div class="flex flex-col items-center gap-1 rounded-lg border p-4">
<div class="h-6 w-6 rounded-full bg-surface-alt"></div>
<div class="h-5 w-12 rounded bg-surface-alt"></div>
<div class="h-4 w-20 rounded bg-surface-alt"></div>
</div>
{% endfor %}
</div>
</section>
<!-- Placeholder: Brewing charts -->
<section>
<div class="mb-5"><div class="h-5 w-24 rounded bg-surface-alt"></div></div>
<div class="grid gap-5 md:grid-cols-2">
{% for _ in 0..2 %}
<div class="rounded-lg border bg-surface p-5">
<div class="h-4 w-28 rounded bg-surface-alt mb-4"></div>
<div class="flex flex-col gap-2">
{% for _ in 0..4 %}
<div class="flex items-center gap-3">
<div class="w-28 h-3 rounded bg-surface-alt"></div>
<div class="flex-1 h-5 rounded bg-surface-alt"></div>
<div class="w-6 h-3 rounded bg-surface-alt"></div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</section>
</div>
<div class="absolute inset-0 z-10 flex items-center justify-center">
<span class="text-lg font-semibold text-text-muted">No stats yet</span>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}