Archived
Catering, and a pure Java/Spring site: Thymeleaf front to back #10
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user