brewlog/templates/pages/home.html

205 lines
8.4 KiB
HTML

{% extends "base.html" %} {% import "partials/bag_card.html" as bag_card %} {% import
"partials/brew_card.html" as brew_card %} {% import "partials/icons.html" as icons %} {% import
"partials/scan_input.html" as scan %} {% block title %}Brewlog{% endblock %} {% block head %} {% if
is_authenticated %}
<script>
const closeBag = async (bagId, cardEl) => {
if (!confirm("Close this bag? This will mark it as finished.")) return
try {
const today = new Date().toISOString().split("T")[0]
const resp = await fetch(`/api/v1/bags/${bagId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ closed: true, remaining: 0.0, finished_at: today }),
})
if (!resp.ok) throw new Error(`Server returned ${resp.status}`)
cardEl.remove()
const grid = document.getElementById("open-bags-grid")
if (grid && grid.children.length === 0) {
document.getElementById("open-bags-section")?.remove()
}
} catch (e) {
alert(`Failed to close bag: ${e.message}`)
}
}
</script>
{% endif %} {% endblock %} {% block content %}
<!-- Scan Bag -->
{% if is_authenticated %}
<section
data-signals:_extracting="false"
data-signals:_extract-error="''"
data-signals:_scan-extracted="false"
data-signals:_scan-submitting="false"
data-signals:_scan-error="''"
data-signals:_roaster-name="''"
data-signals:_roaster-country="''"
data-signals:_roaster-city="''"
data-signals:_roaster-homepage="''"
data-signals:_roast-name="''"
data-signals:_origin="''"
data-signals:_region="''"
data-signals:_producer="''"
data-signals:_process="''"
data-signals:_tasting-notes="''"
data-signals:_open-bag="true"
data-signals:_bag-amount="250"
data-signals:_matched-roaster-id="''"
data-signals:_matched-roast-id="''"
>
<!-- Input: photo or text (shown when not yet extracted) -->
<div data-show="!$_scanExtracted" class="rounded-lg border bg-surface p-5">
<form
id="scan-extract-form"
data-on:submit="$_extracting = true; $_extractError = ''; @post('/api/v1/extract-bag-scan', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_extracting) return; if (evt.detail.type === 'finished') { $_extracting = false; $_scanExtracted = true; document.getElementById('scan-extract-form').reset() } else if (evt.detail.type === 'error') { $_extracting = false; $_extractError = 'Extraction failed. Please try again.' }"
>
{{ scan::scan_input("scan-extract-form", "scan-image", "$_extracting", "$_extractError",
"Describe a coffee bag…", "Waiting for response…") }}
</form>
</div>
<!-- Form: pre-filled roaster + roast (shown after extraction) -->
<div data-show="$_scanExtracted" style="display: none">
<form
class="rounded-lg border bg-surface p-5 flex flex-col gap-6"
data-on:submit="$_scanSubmitting = true; $_scanError = ''; @post('/api/v1/scan', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_scanSubmitting) return; if (evt.detail.type === 'finished') { window.location.reload() } else if (evt.detail.type === 'error') { $_scanSubmitting = false; $_scanError = 'Save failed. Please try again.' }"
>
{% include "partials/forms/scan_result_form.html" %}
</form>
</div>
</section>
{% endif %}
<!-- Recent Brews -->
{% if !recent_brews.is_empty() %}
<section data-signals:_brewing="false">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-text">Recent Brews</h2>
<a href="/data?type=brews" class="text-sm text-accent hover:text-accent-hover font-medium"
>View all brews &rarr;</a
>
</div>
<div class="flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory scrollbar-hide">
{% for brew in recent_brews %} {{ brew_card::card(brew, is_authenticated) }} {% endfor %}
</div>
</section>
{% endif %}
<!-- Open Bags -->
{% if !open_bags.is_empty() %}
<section id="open-bags-section">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-text">Open Bags</h2>
<a href="/data?type=bags" class="text-sm text-accent hover:text-accent-hover font-medium"
>View all bags &rarr;</a
>
</div>
<div
id="open-bags-grid"
class="flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory scrollbar-hide"
>
{% for bag in open_bags %} {{ bag_card::card(bag, is_authenticated) }} {% endfor %}
</div>
</section>
{% endif %}
<!-- Recent Activity -->
{% if !recent_events.is_empty() %}
<section>
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-text">Recent Activity</h2>
<a href="/timeline" class="text-sm text-accent hover:text-accent-hover font-medium"
>View full timeline &rarr;</a
>
</div>
<div class="space-y-2">
{% for event in recent_events %}
<div class="rounded-lg border bg-surface px-4 py-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<span class="pill pill-muted w-28 shrink-0 justify-center whitespace-nowrap"
>{{ event.kind_label }}</span
>
<p class="min-w-0 truncate text-sm">
<a href="{{ event.link }}" class="font-medium text-text hover:text-accent"
>{{ event.title }}</a
>{% if let Some(sub) = event.subtitle %}
<span class="text-xs text-text-muted italic">· {{ sub }}</span>{% endif %}
</p>
</div>
<time class="text-xs text-text-muted whitespace-nowrap shrink-0"
>{{ event.relative_date_label }}</time
>
</div>
{% endfor %}
</div>
</section>
{% endif %}
<!-- Stats -->
<section>
<div class="flex items-center justify-between mb-5">
<h2 class="text-lg font-semibold text-text">Stats</h2>
</div>
<div class="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-7">
<a
href="/data?type=brews"
class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt"
>
{{ icons::beaker("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.brews }}</span>
<span class="text-xs text-text-muted">Brews</span>
</a>
<a
href="/data?type=roasts"
class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt"
>
{{ icons::fire("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.roasts }}</span>
<span class="text-xs text-text-muted">Roasts</span>
</a>
<a
href="/data?type=roasters"
class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt"
>
{{ icons::building("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.roasters }}</span>
<span class="text-xs text-text-muted">Roasters</span>
</a>
<a
href="/data?type=bags"
class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt"
>
{{ icons::bag("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.bags }}</span>
<span class="text-xs text-text-muted">Bags</span>
</a>
<a
href="/data?type=cups"
class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt"
>
{{ icons::cup("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.cups }}</span>
<span class="text-xs text-text-muted">Cups</span>
</a>
<a
href="/data?type=cafes"
class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt"
>
{{ icons::location("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.cafes }}</span>
<span class="text-xs text-text-muted">Cafes</span>
</a>
<a
href="/data?type=gear"
class="group flex flex-col items-center gap-1 rounded-lg border bg-surface p-4 transition hover:bg-surface-alt"
>
{{ icons::wrench("h-6 w-6 text-accent") }}
<span class="text-lg font-bold text-text group-hover:text-accent">{{ stats.gear }}</span>
<span class="text-xs text-text-muted">Gear</span>
</a>
</div>
</section>
{% endblock %}