Catering, and a pure Java/Spring site: Thymeleaf front to back #10

Merged
austin merged 22 commits from feature/catering-tables into main 2026-07-26 21:26:57 -05:00
5 changed files with 84 additions and 8 deletions
Showing only changes of commit e2b9af56f1 - Show all commits
@@ -55,13 +55,18 @@
</div>
</header>
<!--/* One list of links, worn two ways — so the phone and the desktop can't end up offering different
pages. */-->
<!--/*
One list of links, worn two ways — so the phone and the desktop can't end up offering different pages.
The page you are on is marked: aria-current for a screen reader, and a sage rule under the word for
everyone else. A four-page site can get away without it, right up until someone clicks Catering, lands
on a page of prices, and has no idea whether the click worked.
*/-->
<th:block th:fragment="links(itemClass)">
<a href="/products" th:class="${itemClass}">Our Products</a>
<a href="/catering" th:class="${itemClass}">Catering</a>
<a href="/history" th:class="${itemClass}">Our Story</a>
<a href="/contact" th:class="${itemClass}">Contact</a>
<a th:each="item : ${ {{'/products','Our Products'},{'/catering','Catering'},{'/history','Our Story'},{'/contact','Contact'}} }"
th:href="${item[0]}" th:text="${item[1]}"
th:aria-current="${path == item[0] ? 'page' : null}"
th:class="${itemClass} + (${path == item[0]} ? ' is-here' : '')">Our Products</a>
</th:block>
</body>
</html>
@@ -15,8 +15,11 @@
<head th:replace="~{fragments/head :: head(${title}, ${description}, ${path})}"></head>
<body>
<div class="min-h-screen bg-bakery-50 flex flex-col">
<!--/* Hidden until it has focus: the first thing a keyboard reaches, so the nav isn't a toll gate on
every page. */-->
<a href="#content" class="skip-link">Skip to the page</a>
<div th:replace="~{fragments/header :: header}"></div>
<main class="flex-grow">
<main id="content" class="flex-grow">
<div th:replace="${content}"></div>
</main>
<div th:replace="~{fragments/footer :: footer}"></div>
+17 -1
View File
@@ -26,7 +26,7 @@
<div class="measure space-y-12">
<section>
<h2 class="h3 text-bakery-900 mb-4">How it started</h2>
<p class="text-bakery-800 leading-relaxed">
<p class="opening-para text-bakery-800 leading-relaxed">
Morissa Bennett opened The Vine in 2024, at 215 E Main Street, in the middle of downtown
Princeville. The plan was not complicated. Bake it ourselves, sell it ourselves, and keep
enough tables that nobody feels rushed out the door.
@@ -42,6 +42,14 @@
</p>
</section>
<figure>
<img th:src="${photos.of('gallery/Cinnamon Rolls.webp')}"
alt="Cinnamon rolls, iced, on the counter at The Vine"
width="1200" height="800" loading="lazy"
class="photo w-full h-64 md:h-80 object-cover">
<figcaption class="mt-3 label text-bakery-500 text-center">The cinnamon rolls, most mornings</figcaption>
</figure>
<section>
<h2 class="h3 text-bakery-900 mb-4">The cakes are the fun part</h2>
<p class="text-bakery-800 leading-relaxed">
@@ -53,6 +61,14 @@
</p>
</section>
<figure>
<img th:src="${photos.of('gallery/Cakes.webp')}"
alt="A decorated cake made to order at The Vine"
width="1200" height="800" loading="lazy"
class="photo w-full h-64 md:h-80 object-cover">
<figcaption class="mt-3 label text-bakery-500 text-center">Made to order, whatever the week is celebrating</figcaption>
</figure>
<section>
<h2 class="h3 text-bakery-900 mb-4">Around town</h2>
<p class="text-bakery-800 leading-relaxed">
+34
View File
@@ -200,3 +200,37 @@
@utility band {
@apply py-14 md:py-20;
}
/*
* THE PAGE YOU ARE ON, in the nav. A rule under the word rather than a colour change: the nav is already
* sage on cream, and darkening it further reads as a hover, not as "you are here".
*/
@utility is-here {
@apply text-bakery-900;
text-decoration: underline;
text-decoration-color: var(--color-bakery-500);
text-decoration-thickness: 2px;
text-underline-offset: 8px;
}
/*
* The keyboard's way past the nav. Off-screen until focused, then a normal-looking button in the corner —
* the pattern only works if it becomes visible, which is the half everyone forgets.
*/
@utility skip-link {
@apply sr-only;
&:focus {
@apply not-sr-only fixed left-4 top-4 z-50 pill-sage;
}
}
/*
* An opening paragraph set like a printed one. Used once, on the story — a drop cap on every page would
* be a costume rather than a voice.
*/
@utility opening-para {
&::first-letter {
@apply font-adbhashitha text-6xl text-bakery-700 float-left mr-3 mt-1;
line-height: 0.75;
}
}
@@ -123,6 +123,24 @@ class SiteControllerTest {
mvc.perform(get("/products")).andExpect(content().string(containsString("href=\"/catering\"")));
}
@Test
void theNavMarksThePageYouAreOn() throws Exception {
// Generated from the path the controller set, so a wrong model attribute would mark nothing at
// all — and nothing at all looks exactly like a page that simply has no active link.
mvc.perform(get("/catering"))
.andExpect(content().string(containsString("href=\"/catering\" aria-current=\"page\"")));
mvc.perform(get("/products"))
.andExpect(content().string(containsString("href=\"/products\" aria-current=\"page\"")))
.andExpect(content().string(not(containsString("href=\"/catering\" aria-current"))));
}
@Test
void everyPageOffersTheKeyboardAWayPastTheNav() throws Exception {
mvc.perform(get("/"))
.andExpect(content().string(containsString("href=\"#content\"")))
.andExpect(content().string(containsString("id=\"content\"")));
}
@Test
void aCateringButtonStartsTheEnquiryOffAboutThatTable() throws Exception {
mvc.perform(get("/contact").param("about", "Weddings"))