This repository has been archived on 2026-09-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
itsthevine/src/main/resources/templates/products.html
T
austin e35434a609 Sections and cards: three grids, two quiet panels and five headings become three names
Counted first, as with the last sweep. A grid of cards was written three times with three different sets
of breakpoints and gaps, so cards on different pages lined up differently for no reason anyone chose. A
tinted aside card existed twice, hand-rolled, with different borders. A section heading existed five ways:
centred with one margin, centred with another, left-aligned with a blurb, left without.

Now `card-grid`, `panel-quiet` and a `section-head` fragment (with a cream-on-sage twin for the dark
bands). The catering small print and the contact page's "ordering for a crowd?" card are the same object
now, which is what they always were.

TWO THINGS THE SCREENSHOTS DECIDED, not taste:

Three prices go three-up from md, while the shared card grid waits for lg. A grid of photos is fine
two-wide; three prices are a comparison, and 2 + 1 on a laptop strands the third size on its own row.

And the per-table column count is entirely in one classappend rather than overriding `card-grid`, because
Tailwind emits grid-cols-1, -2, -3 in that order — so a `grid-cols-3` from the shared class beats a
`grid-cols-2` written beside it in the markup, whatever the markup says. Two classes for one property,
winner decided by the stylesheet: the same trap that stopped `hidden` from hiding the open-now line, and I
had already written it before catching it. A one-size and a two-size table were both wrong; proved the fix
by building a real two-size table through the admin and looking at it, then deleting it again.

61 tests.
2026-07-26 21:15:37 -05:00

95 lines
4.4 KiB
HTML

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/page :: page(${title}, ${description}, ${path}, ~{::content})}">
<body>
<div th:fragment="content" class="min-h-screen bg-bakery-50">
<div th:replace="~{fragments/opening :: plain(
'From the counter',
'Our products',
'What comes out of the kitchen on Main Street. Cakes and decorated cookies are made to order, so most of what follows started as somebody describing what they wanted.')}"></div>
<div class="container pb-16">
<!--/*
The category filter: links, not buttons. Each filtered view is now a URL you can bookmark or send
to somebody, the browser's back button does what it looks like it does, and a crawler can see all
forty items instead of whichever twelve the default filter showed. The filtering itself has always
been the server's job — this just stopped pretending otherwise.
*/-->
<div class="filter-row mb-12">
<a th:each="category : ${categories}"
th:href="${category == 'All' ? '/products' : '/products?category=' + #uris.escapeQueryParam(category)}"
th:text="${category}"
th:aria-current="${category == selected ? 'page' : null}"
th:class="${category == selected} ? 'chip-on' : 'chip'">All</a>
</div>
<!--/* Deliberately unanimated, as before: filtering used to run a layout reflow plus an enter/exit
fade on every card, which on a 40-card grid reads as the page lurching rather than
responding. The only motion left is the shadow on hover. */-->
<div class="card-grid">
<div th:each="product : ${products}"
class="panel overflow-hidden">
<div class="relative w-full overflow-hidden">
<div th:replace="~{:: gallery(${product})}"></div>
</div>
<!--/* The caption sits on the cream, so the photo reads as mounted on the card rather than as
the card itself. */-->
<div class="panel-head border-b-0 border-t px-6 py-5 text-center">
<h3 class="h4 text-bakery-900" th:text="${product.name}">Name</h3>
<span class="label text-bakery-600" th:text="${product.category}">Category</span>
</div>
</div>
</div>
<p th:if="${#lists.isEmpty(products)}" class="text-center text-bakery-800">
Nothing in that category just now.
<a href="/products" class="link-plain">See everything</a>.
</p>
</div>
<!--/* A catalogue with no way to order from it is a gallery. */-->
<section class="section bg-bakery-800 text-white no-print">
<div class="container">
<div class="measure text-center">
<h2 class="h3">Want one of these, or something else?</h2>
<p class="mt-4 text-bakery-100 leading-relaxed">
Cakes and decorated cookies are made to order. Tell us the date and roughly what you have in
mind — a tractor, a retirement, the class of 1964 — and we will work from there.
</p>
<div class="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<a href="/contact"
class="pill-cream">
Ask about an order
</a>
<a href="/catering"
class="pill-ghost">
Ordering for a crowd
</a>
</div>
</div>
</div>
</section>
</div>
<!--/*
The photo on a card.
A scroll-snap strip rather than a stack of absolutely positioned images cross-fading: it is one square
photo at a time either way, but this version swipes on a phone and scrolls with a trackpad with no
script at all. /js/gallery.js adds the arrows and the dots on top. They are created there rather than
rendered here on purpose — a control that does nothing without JavaScript is worse than no control.
*/-->
<div th:fragment="gallery(product)" class="relative aspect-square bg-bakery-100">
<div class="gallery flex h-full w-full overflow-x-auto snap-x snap-mandatory no-scrollbar"
th:attr="aria-label=${#lists.size(product.images) > 1 ? product.name + ' — ' + #lists.size(product.images) + ' photos' : null}"
th:aria-roledescription="${#lists.size(product.images) > 1 ? 'carousel' : null}">
<img th:each="image, i : ${product.images}" th:src="${image}"
th:alt="${i.index == 0 ? product.name : ''}"
th:aria-hidden="${i.index == 0 ? null : 'true'}"
loading="lazy" decoding="async"
class="snap-center shrink-0 h-full w-full object-cover">
</div>
</div>
</body>
</html>