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:
parent
10c19b1f67
commit
98c8391670
4 changed files with 62 additions and 114 deletions
|
|
@ -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">
|
<section data-signals:_show-form="false" data-signals:_is-submitting="false">
|
||||||
<header class="flex flex-wrap items-start justify-between gap-4">
|
<header class="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
{% block head %}
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
|
|
@ -276,36 +276,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="open-bags-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<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 %}
|
{% 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">
|
{% call bag_card::card(bag, is_authenticated) %}
|
||||||
<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>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
32
templates/partials/bag_card.html
Normal file
32
templates/partials/bag_card.html
Normal 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 %}
|
||||||
|
|
@ -1,91 +1,13 @@
|
||||||
{% import "partials/table.html" as table %}
|
{% 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">
|
<div id="bag-list" class="mt-6 space-y-8" data-star-scope="bags">
|
||||||
{% if !open_bags.is_empty() %}
|
{% if !open_bags.is_empty() %}
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-2xl font-bold mb-4 text-stone-800">Open Bags</h2>
|
<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-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{% for bag in open_bags %}
|
{% for bag in open_bags %}
|
||||||
<div class="bg-white rounded-lg shadow-md overflow-hidden border border-stone-200">
|
{% call bag_card::card(bag, is_authenticated) %}
|
||||||
<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>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue