refactor(templates): extract shared bag card partial for open bags

- Create bag_card.html macro used by both home page and bags page
- Replace divergent card styles with consistent amber-themed design
- Add closeBag() JS to bags page for closing bags with confirmation
- Use "Close" button with cross icon consistently across both pages
This commit is contained in:
Jon Seager 2026-02-04 16:01:57 +00:00
parent 10c19b1f67
commit 98c8391670
No known key found for this signature in database
4 changed files with 62 additions and 114 deletions

View file

@ -1,4 +1,27 @@
{% extends "base.html" %} {% block title %}Brewlog · Bags{% endblock %} {% block content %}
{% extends "base.html" %} {% block title %}Brewlog · Bags{% endblock %}
{% block head %}
<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}`);
window.location.reload();
} catch (e) {
alert(`Failed to close bag: ${e.message}`);
}
};
</script>
{% endblock %}
{% block content %}
<section data-signals:_show-form="false" data-signals:_is-submitting="false">
<header class="flex flex-wrap items-start justify-between gap-4">
<div class="flex flex-col gap-2">

View file

@ -1,4 +1,4 @@
{% extends "base.html" %} {% block title %}Brewlog{% endblock %}
{% extends "base.html" %} {% import "partials/bag_card.html" as bag_card %} {% block title %}Brewlog{% endblock %}
{% block head %}
{% if is_authenticated %}
@ -276,36 +276,7 @@
</div>
<div id="open-bags-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for bag in open_bags %}
<div id="bag-card-{{ bag.id }}" class="rounded-lg border border-amber-200 bg-amber-50 p-4 shadow-sm">
<div>
<a href="/roasters/{{ bag.roaster_slug }}/roasts/{{ bag.roast_slug }}" class="font-semibold text-amber-800 hover:text-amber-600">{{ bag.roast_name }}</a>
<p class="text-sm text-stone-500">{{ bag.roaster_name }}</p>
</div>
<p class="mt-2 text-sm text-stone-600">
<span class="font-medium">{{ bag.remaining }}g</span>
<span class="text-stone-400">of {{ bag.amount }}g remaining</span>
</p>
{% if is_authenticated %}
<div class="mt-3 pt-3 border-t border-amber-100 flex gap-3">
<a href="/brews?bag_id={{ bag.id }}" class="inline-flex items-center gap-1 text-sm font-medium text-amber-700 hover:text-amber-500">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm.75-11.25a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z" clip-rule="evenodd" />
</svg>
Brew
</a>
<button
type="button"
onclick="closeBag('{{ bag.id }}', document.getElementById('bag-card-{{ bag.id }}'))"
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-stone-700"
>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
Close
</button>
</div>
{% endif %}
</div>
{% call bag_card::card(bag, is_authenticated) %}
{% endfor %}
</div>
</section>

View file

@ -0,0 +1,32 @@
{% macro card(bag, is_authenticated) %}
<div id="bag-card-{{ bag.id }}" class="rounded-lg border border-amber-200 bg-amber-50 p-4 shadow-sm">
<div>
<a href="/roasters/{{ bag.roaster_slug }}/roasts/{{ bag.roast_slug }}" class="font-semibold text-amber-800 hover:text-amber-600">{{ bag.roast_name }}</a>
<p class="text-sm text-stone-500">{{ bag.roaster_name }}</p>
</div>
<p class="mt-2 text-sm text-stone-600">
<span class="font-medium">{{ bag.remaining }}g</span>
<span class="text-stone-400">of {{ bag.amount }}g remaining</span>
</p>
{% if is_authenticated %}
<div class="mt-3 pt-3 border-t border-amber-100 flex gap-3">
<a href="/brews?bag_id={{ bag.id }}" class="inline-flex items-center gap-1 text-sm font-medium text-amber-700 hover:text-amber-500">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm.75-11.25a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z" clip-rule="evenodd" />
</svg>
Brew
</a>
<button
type="button"
class="inline-flex items-center gap-1 text-sm font-medium text-stone-500 hover:text-stone-700"
onclick="closeBag('{{ bag.id }}', document.getElementById('bag-card-{{ bag.id }}'))"
>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
Close
</button>
</div>
{% endif %}
</div>
{% endmacro %}

View file

@ -1,91 +1,13 @@
{% import "partials/table.html" as table %}
{% import "partials/bag_card.html" as bag_card %}
<div id="bag-list" class="mt-6 space-y-8" data-star-scope="bags">
{% if !open_bags.is_empty() %}
<section>
<h2 class="text-2xl font-bold mb-4 text-stone-800">Open Bags</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<h2 class="text-lg font-semibold text-amber-700 mb-3">Open Bags</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for bag in open_bags %}
<div class="bg-white rounded-lg shadow-md overflow-hidden border border-stone-200">
<div class="p-6">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-xl font-semibold text-stone-900">
<a
href="/roasters/{{ bag.roaster_slug }}"
class="hover:text-amber-700 hover:underline"
>
{{ bag.roaster_name }}
</a>
</h3>
<p class="text-stone-600">
<a
href="/roasters/{{ bag.roaster_slug }}/roasts/{{ bag.roast_slug }}"
class="hover:text-amber-700 hover:underline"
>
{{ bag.roast_name }}
</a>
</p>
</div>
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
Open
</span>
</div>
<div class="space-y-2 text-sm text-stone-500 mb-4">
<div class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"
/>
</svg>
<span>{{ bag.remaining }} / {{ bag.amount }}g</span>
</div>
<div class="flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{% if let Some(roast_date) = bag.roast_date %}
<span>Roasted {{ roast_date }}</span>
{% else %}
<span>Added {{ bag.created_at }}</span>
{% endif %}
</div>
</div>
{% if is_authenticated %}
<div class="pt-4 border-t border-stone-100 flex justify-end">
<button
class="text-sm text-stone-600 hover:text-stone-900 font-medium"
data-on:click="@put('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}&closed=true&remaining=0.0&finished_at=' + new Date().toISOString().split('T')[0], {responseOverrides: {selector: '#bag-list', mode: 'replace'}})"
>
Finish Bag
</button>
</div>
{% endif %}
</div>
</div>
{% call bag_card::card(bag, is_authenticated) %}
{% endfor %}
</div>
</section>