- Add new icon macros: coffee_bean, grinder, cup (redesigned), pencil, clipboard, check, x_mark, arrow_down_tray, arrow_up_tray - Remove orphaned building and wrench macros - Remap entity icons: roast→coffee_bean, roaster→fire, gear→grinder - Replace pill labels with entity-type icons in homepage activity rows - Add entity-type icons to timeline cards and tab buttons - Fix brew/bag card action icons (cup→beaker) - Add entity_type field to TimelineEventView, add roaster detail picks
295 lines
13 KiB
HTML
295 lines
13 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 %} {% 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="''"
|
|
>
|
|
<!-- Quick actions (shown when not yet extracted) -->
|
|
<div data-show="!$_scanExtracted">
|
|
<div data-show="!$_extracting" class="grid grid-cols-3 gap-4">
|
|
<input type="hidden" name="image" id="scan-image" form="scan-extract-form" />
|
|
<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.' }"
|
|
class="hidden"></form>
|
|
<brew-photo-capture target-input="scan-image" target-form="scan-extract-form"
|
|
class="inline-flex flex-col items-center justify-center gap-1.5 rounded-md border px-4 py-3 text-accent transition hover:bg-surface-alt cursor-pointer"
|
|
aria-label="Scan Bag"
|
|
>
|
|
{{ icons::camera("h-5 w-5") }}
|
|
<span class="text-sm font-medium">Scan</span>
|
|
</brew-photo-capture>
|
|
<a href="/check-in"
|
|
class="inline-flex flex-col items-center justify-center gap-1.5 rounded-md border px-4 py-3 text-accent transition hover:bg-surface-alt"
|
|
title="Check in at a cafe"
|
|
aria-label="Check in at a cafe"
|
|
>
|
|
{{ icons::location("h-5 w-5") }}
|
|
<span class="text-sm font-medium">Check In</span>
|
|
</a>
|
|
<a href="/add"
|
|
class="inline-flex flex-col items-center justify-center gap-1.5 rounded-md border px-4 py-3 text-accent transition hover:bg-surface-alt"
|
|
title="Add manually"
|
|
aria-label="Add manually"
|
|
>
|
|
{{ icons::plus("h-5 w-5") }}
|
|
<span class="text-sm font-medium">Add</span>
|
|
</a>
|
|
</div>
|
|
<div data-show="$_extracting" style="display:none" class="flex items-center justify-center gap-3 text-sm text-accent">
|
|
{{ icons::spinner("h-5 w-5") }}
|
|
Waiting for response…
|
|
</div>
|
|
<p data-show="$_extractError" data-text="$_extractError" style="display:none" class="mt-2 text-sm text-red-600 text-center"></p>
|
|
</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 -->
|
|
<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 →</a
|
|
>
|
|
</div>
|
|
{% if !recent_brews.is_empty() %}
|
|
<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>
|
|
{% else %}
|
|
<div class="relative">
|
|
<div class="flex gap-4 overflow-hidden blur-[2px] select-none pointer-events-none" aria-hidden="true">
|
|
{% for _ in 0..6 %}
|
|
<div class="w-[45vw] md:w-[200px] h-[279px] shrink-0 rounded-lg border bg-surface p-4 flex flex-col">
|
|
<div class="h-[4.5rem]">
|
|
<div class="h-4 w-3/4 rounded bg-surface-alt"></div>
|
|
<div class="mt-2 h-3 w-1/2 rounded bg-surface-alt"></div>
|
|
</div>
|
|
<div class="mt-2 flex-1 space-y-2">
|
|
<div class="h-3 w-full rounded bg-surface-alt"></div>
|
|
<div class="h-3 w-5/6 rounded bg-surface-alt"></div>
|
|
<div class="h-3 w-2/3 rounded bg-surface-alt"></div>
|
|
<div class="h-3 w-3/4 rounded bg-surface-alt"></div>
|
|
</div>
|
|
<div class="mt-3 h-8 rounded-md 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 brews yet</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Open Bags -->
|
|
<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 →</a
|
|
>
|
|
</div>
|
|
{% if !open_bags.is_empty() %}
|
|
<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>
|
|
{% else %}
|
|
<div class="relative">
|
|
<div class="flex gap-4 overflow-hidden blur-[2px] select-none pointer-events-none" aria-hidden="true">
|
|
{% for _ in 0..6 %}
|
|
<div class="w-[45vw] md:w-[200px] h-[200px] shrink-0 rounded-lg border bg-surface p-4 flex flex-col">
|
|
<div class="flex-1">
|
|
<div class="h-4 w-3/4 rounded bg-surface-alt"></div>
|
|
<div class="mt-2 h-3 w-1/2 rounded bg-surface-alt"></div>
|
|
</div>
|
|
<div>
|
|
<div class="h-2 w-full rounded-full bg-surface-alt"></div>
|
|
<div class="mt-1 h-2 w-1/3 rounded bg-surface-alt"></div>
|
|
</div>
|
|
<div class="mt-3 flex gap-2">
|
|
<div class="flex-1 h-8 rounded-md bg-surface-alt"></div>
|
|
<div class="flex-1 h-8 rounded-md bg-surface-alt"></div>
|
|
</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 open bags</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Recent Activity -->
|
|
<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 →</a
|
|
>
|
|
</div>
|
|
{% if !recent_events.is_empty() %}
|
|
<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="text-text-muted shrink-0">
|
|
{% if event.entity_type == "brew" %}{{ icons::beaker("h-5 w-5") }}{% elif event.entity_type == "roast" %}{{ icons::coffee_bean("h-5 w-5") }}{% elif event.entity_type == "roaster" %}{{ icons::fire("h-5 w-5") }}{% elif event.entity_type == "bag" %}{{ icons::bag("h-5 w-5") }}{% elif event.entity_type == "cup" %}{{ icons::cup("h-5 w-5") }}{% elif event.entity_type == "cafe" %}{{ icons::location("h-5 w-5") }}{% elif event.entity_type == "gear" %}{{ icons::grinder("h-5 w-5") }}{% else %}{{ icons::beaker("h-5 w-5") }}{% endif %}
|
|
</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">· {{ sub }}</span>{% endif %}
|
|
</p>
|
|
</div>
|
|
<time class="text-xs text-text-muted whitespace-nowrap shrink-0"
|
|
>{{ event.relative_date_label }}</time
|
|
>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="relative">
|
|
<div class="space-y-2 blur-[2px] select-none pointer-events-none" aria-hidden="true">
|
|
{% for _ in 0..3 %}
|
|
<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">
|
|
<div class="h-5 w-5 shrink-0 rounded-full bg-surface-alt"></div>
|
|
<div class="h-4 w-40 rounded bg-surface-alt"></div>
|
|
</div>
|
|
<div class="h-3 w-16 shrink-0 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 activity yet</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- 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 p-4 transition hover:bg-surface-alt"
|
|
>
|
|
{{ icons::beaker("h-6 w-6 text-accent") }}
|
|
<span class="text-lg font-bold text-text">{{ stats.brews }}</span>
|
|
<span class="text-sm font-medium text-accent">Brews</span>
|
|
</a>
|
|
<a
|
|
href="/data?type=roasts"
|
|
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
|
|
>
|
|
{{ icons::coffee_bean("h-6 w-6 text-accent") }}
|
|
<span class="text-lg font-bold text-text">{{ stats.roasts }}</span>
|
|
<span class="text-sm font-medium text-accent">Roasts</span>
|
|
</a>
|
|
<a
|
|
href="/data?type=roasters"
|
|
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
|
|
>
|
|
{{ icons::fire("h-6 w-6 text-accent") }}
|
|
<span class="text-lg font-bold text-text">{{ stats.roasters }}</span>
|
|
<span class="text-sm font-medium text-accent">Roasters</span>
|
|
</a>
|
|
<a
|
|
href="/data?type=bags"
|
|
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
|
|
>
|
|
{{ icons::bag("h-6 w-6 text-accent") }}
|
|
<span class="text-lg font-bold text-text">{{ stats.bags }}</span>
|
|
<span class="text-sm font-medium text-accent">Bags</span>
|
|
</a>
|
|
<a
|
|
href="/data?type=cups"
|
|
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
|
|
>
|
|
{{ icons::cup("h-6 w-6 text-accent") }}
|
|
<span class="text-lg font-bold text-text">{{ stats.cups }}</span>
|
|
<span class="text-sm font-medium text-accent">Cups</span>
|
|
</a>
|
|
<a
|
|
href="/data?type=cafes"
|
|
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
|
|
>
|
|
{{ icons::location("h-6 w-6 text-accent") }}
|
|
<span class="text-lg font-bold text-text">{{ stats.cafes }}</span>
|
|
<span class="text-sm font-medium text-accent">Cafes</span>
|
|
</a>
|
|
<a
|
|
href="/data?type=gear"
|
|
class="group flex flex-col items-center gap-1 rounded-lg border p-4 transition hover:bg-surface-alt"
|
|
>
|
|
{{ icons::grinder("h-6 w-6 text-accent") }}
|
|
<span class="text-lg font-bold text-text">{{ stats.gear }}</span>
|
|
<span class="text-sm font-medium text-accent">Gear</span>
|
|
</a>
|
|
</div>
|
|
</section>
|
|
{% endblock %}
|