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> </div>
</header> </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)"> <th:block th:fragment="links(itemClass)">
<a href="/products" th:class="${itemClass}">Our Products</a> <a th:each="item : ${ {{'/products','Our Products'},{'/catering','Catering'},{'/history','Our Story'},{'/contact','Contact'}} }"
<a href="/catering" th:class="${itemClass}">Catering</a> th:href="${item[0]}" th:text="${item[1]}"
<a href="/history" th:class="${itemClass}">Our Story</a> th:aria-current="${path == item[0] ? 'page' : null}"
<a href="/contact" th:class="${itemClass}">Contact</a> th:class="${itemClass} + (${path == item[0]} ? ' is-here' : '')">Our Products</a>
</th:block> </th:block>
</body> </body>
</html> </html>
@@ -15,8 +15,11 @@
<head th:replace="~{fragments/head :: head(${title}, ${description}, ${path})}"></head> <head th:replace="~{fragments/head :: head(${title}, ${description}, ${path})}"></head>
<body> <body>
<div class="min-h-screen bg-bakery-50 flex flex-col"> <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> <div th:replace="~{fragments/header :: header}"></div>
<main class="flex-grow"> <main id="content" class="flex-grow">
<div th:replace="${content}"></div> <div th:replace="${content}"></div>
</main> </main>
<div th:replace="~{fragments/footer :: footer}"></div> <div th:replace="~{fragments/footer :: footer}"></div>
+17 -1
View File
@@ -26,7 +26,7 @@
<div class="measure space-y-12"> <div class="measure space-y-12">
<section> <section>
<h2 class="h3 text-bakery-900 mb-4">How it started</h2> <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 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 Princeville. The plan was not complicated. Bake it ourselves, sell it ourselves, and keep
enough tables that nobody feels rushed out the door. enough tables that nobody feels rushed out the door.
@@ -42,6 +42,14 @@
</p> </p>
</section> </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> <section>
<h2 class="h3 text-bakery-900 mb-4">The cakes are the fun part</h2> <h2 class="h3 text-bakery-900 mb-4">The cakes are the fun part</h2>
<p class="text-bakery-800 leading-relaxed"> <p class="text-bakery-800 leading-relaxed">
@@ -53,6 +61,14 @@
</p> </p>
</section> </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> <section>
<h2 class="h3 text-bakery-900 mb-4">Around town</h2> <h2 class="h3 text-bakery-900 mb-4">Around town</h2>
<p class="text-bakery-800 leading-relaxed"> <p class="text-bakery-800 leading-relaxed">
+34
View File
@@ -200,3 +200,37 @@
@utility band { @utility band {
@apply py-14 md:py-20; @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\""))); 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 @Test
void aCateringButtonStartsTheEnquiryOffAboutThatTable() throws Exception { void aCateringButtonStartsTheEnquiryOffAboutThatTable() throws Exception {
mvc.perform(get("/contact").param("about", "Weddings")) mvc.perform(get("/contact").param("about", "Weddings"))