Archived
Give the other pages somewhere to go, and move the stylesheets under src/main
MARKETING, not decoration — each of these was a page that stopped:
- The HOMEPAGE never mentioned catering. A customer found the page by reading the nav. It now has a
section with a photo, what the three tables cover, and a button.
- PRODUCTS was a title and a grid. It gets a line saying what it is ("cakes and decorated cookies are
made to order, so most of what follows started as somebody describing what they wanted") and a closing
band with somewhere to go — an order, or the catering page.
- CONTACT was a form and nothing else. The things somebody on that page actually wants — when we're open,
where we are, the phone number — were on the homepage and in the footer but not there. Now beside the
form, with a card pointing at catering.
- OUR STORY got the shop's own front as a band and an ending, so the story leads somewhere.
- The FOOTER gained the opening hours, which is what people come to a bakery's site for and is now on
every page.
CLEANED UP ON THE WAY. The hours and the address were about to exist in three places, so they are one
fragment (fragments/visit.html) that takes the classes that vary by where it sits — cream in the footer,
sage on white in a card. A shop that changes its Saturday hours should change them once.
STYLES MOVED to src/main/styles, with the rest of the app's source, and split by what each file is for:
theme.css (the fonts and the palette — names, nothing drawn), base.css (bare elements), components.css
(the site's own classes), admin.css (the admin's controls), and site.css as the entry that imports them in
cascade order and declares what Tailwind scans. It was one grab-bag file called tokens.css in a folder at
the repo root. node_modules has to stay beside them — Tailwind resolves `@import "tailwindcss"` by walking
up from the CSS file — so the whole npm project moved together, and the pom, .gitignore and README moved
with it.
53 tests green, `mvn package` builds the stylesheet from its new home, and every class used on all five
pages resolves in the compiled CSS (185/159/175/148/140, none missing).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
target/
|
target/
|
||||||
# The Tailwind CLI's dependencies. The compiled stylesheet needs no rule of its own: it is written
|
# The Tailwind CLI's dependencies. The compiled stylesheet needs no rule of its own: it is written
|
||||||
# straight into target/classes/static/css, which is already ignored above.
|
# straight into target/classes/static/css, which is already ignored above.
|
||||||
styles/node_modules/
|
src/main/styles/node_modules/
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ there is no JavaScript framework in this repo. The look has not changed through
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Backend | Spring Boot 4 / Java 25, `com.itsthevine.web` |
|
| Backend | Spring Boot 4 / Java 25, `com.itsthevine.web` |
|
||||||
| Pages | Thymeleaf, `src/main/resources/templates` — **no JavaScript** except one 100-line file for the product-card arrows |
|
| Pages | Thymeleaf, `src/main/resources/templates` — **no JavaScript** except one 100-line file for the product-card arrows |
|
||||||
| Styling | Tailwind v4, compiled from the templates by the Tailwind CLI into `static/css/site.css`. `styles/` is the whole asset pipeline |
|
| Styling | Tailwind v4, compiled from the templates by the Tailwind CLI into `static/css/site.css`. `src/main/styles` is the whole asset pipeline |
|
||||||
| Admin | Thymeleaf forms at `/admin`, behind Authentik |
|
| Admin | Thymeleaf forms at `/admin`, behind Authentik |
|
||||||
| Database | Postgres (`itsthevine` on the shared `app-db` cluster), Flyway |
|
| Database | Postgres (`itsthevine` on the shared `app-db` cluster), Flyway |
|
||||||
| Photos | public MinIO bucket `itsthevine` — **not** in the repo or the image |
|
| Photos | public MinIO bucket `itsthevine` — **not** in the repo or the image |
|
||||||
@@ -120,7 +120,7 @@ history. EXIF (including GPS from phone photos) is stripped by the re-encode.
|
|||||||
mvn spring-boot:run # http://localhost:8080
|
mvn spring-boot:run # http://localhost:8080
|
||||||
|
|
||||||
# the stylesheet, while editing templates — watches and recompiles
|
# the stylesheet, while editing templates — watches and recompiles
|
||||||
cd styles && npm install && npm run watch
|
cd src/main/styles && npm install && npm run watch
|
||||||
```
|
```
|
||||||
|
|
||||||
`/admin` only exists when `SECURITY_MODE=OIDC`, so a plain local run has the site and no admin. To work
|
`/admin` only exists when `SECURITY_MODE=OIDC`, so a plain local run has the site and no admin. To work
|
||||||
|
|||||||
@@ -127,14 +127,16 @@
|
|||||||
<!--
|
<!--
|
||||||
There is no JavaScript application here any more, so node is present for one reason:
|
There is no JavaScript application here any more, so node is present for one reason:
|
||||||
Tailwind needs a compiler, and the alternative to it is a hand-written stylesheet.
|
Tailwind needs a compiler, and the alternative to it is a hand-written stylesheet.
|
||||||
`styles/` is the whole asset pipeline (see styles/site.css).
|
src/main/styles is the whole asset pipeline (see its site.css).
|
||||||
-->
|
-->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.github.eirslett</groupId>
|
<groupId>com.github.eirslett</groupId>
|
||||||
<artifactId>frontend-maven-plugin</artifactId>
|
<artifactId>frontend-maven-plugin</artifactId>
|
||||||
<configuration>
|
<configuration>
|
||||||
<!-- Overrides platform-parent's `frontend`, which was where the SPA lived. -->
|
<!-- Overrides platform-parent's `frontend`, which was where the SPA lived. The
|
||||||
<workingDirectory>styles</workingDirectory>
|
stylesheet sources live with the rest of the app's source; node_modules has to
|
||||||
|
sit beside them, because that is where Tailwind resolves its own import from. -->
|
||||||
|
<workingDirectory>src/main/styles</workingDirectory>
|
||||||
</configuration>
|
</configuration>
|
||||||
<executions>
|
<executions>
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -5,10 +5,19 @@
|
|||||||
|
|
||||||
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
||||||
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Contact us</h1>
|
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Contact us</h1>
|
||||||
|
<p class="mt-4 text-bakery-800 max-w-xl mx-auto leading-relaxed">
|
||||||
|
Ask about a cake, a date, or an order for a crowd. We read everything that comes in.
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!--/*
|
||||||
|
The form, and beside it the things somebody on this page is usually actually after: when we are open,
|
||||||
|
where we are, and the phone number. They were on the homepage and in the footer but not here, which
|
||||||
|
made this page a form and nothing else.
|
||||||
|
*/-->
|
||||||
<div class="container mx-auto px-4 pb-16 md:pb-24">
|
<div class="container mx-auto px-4 pb-16 md:pb-24">
|
||||||
<div class="max-w-2xl mx-auto bg-white p-8 md:p-10 rounded-3xl shadow-xs">
|
<div class="grid gap-8 lg:grid-cols-[1.4fr_1fr] max-w-5xl mx-auto items-start">
|
||||||
|
<div class="bg-white p-8 md:p-10 rounded-3xl shadow-xs">
|
||||||
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-2">Get in touch</h2>
|
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-2">Get in touch</h2>
|
||||||
<p class="text-bakery-700 mb-8">
|
<p class="text-bakery-700 mb-8">
|
||||||
Or call us: <a href="tel:+13097010660" class="underline underline-offset-4 hover:text-bakery-600">(309) 701-0660</a>
|
Or call us: <a href="tel:+13097010660" class="underline underline-offset-4 hover:text-bakery-600">(309) 701-0660</a>
|
||||||
@@ -59,6 +68,30 @@
|
|||||||
<p th:if="${sent}" role="status" class="text-bakery-700">
|
<p th:if="${sent}" role="status" class="text-bakery-700">
|
||||||
Thanks. Your message is on its way, and we will get back to you.
|
Thanks. Your message is on its way, and we will get back to you.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="space-y-6">
|
||||||
|
<div class="bg-white p-6 md:p-8 rounded-3xl shadow-xs">
|
||||||
|
<h2 class="font-adbhashitha text-xl text-bakery-900">Our hours</h2>
|
||||||
|
<div class="mt-4 text-bakery-800"
|
||||||
|
th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-6 md:p-8 rounded-3xl shadow-xs">
|
||||||
|
<h2 class="font-adbhashitha text-xl text-bakery-900">Find us</h2>
|
||||||
|
<div class="mt-4 text-bakery-800"
|
||||||
|
th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-3xl border border-bakery-200 p-6 md:p-8">
|
||||||
|
<h2 class="font-adbhashitha text-xl text-bakery-900">Ordering for a crowd?</h2>
|
||||||
|
<p class="mt-2 text-sm text-bakery-700 leading-relaxed">
|
||||||
|
Goodie boxes, party packages and wedding desserts have their own page, with sizes and prices.
|
||||||
|
</p>
|
||||||
|
<a href="/catering" class="mt-4 inline-block text-bakery-700 hover:text-bakery-900 font-medium
|
||||||
|
underline underline-offset-4 transition-colors">
|
||||||
|
Goodie boxes & catering
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,13 +20,11 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Visit</h3>
|
<h3 class="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Visit</h3>
|
||||||
<address class="not-italic space-y-2">
|
<div th:replace="~{fragments/visit :: address('hover:text-white transition')}"></div>
|
||||||
<p>215 E Main Street<br>Princeville, IL 61559</p>
|
<!--/* The hours are the thing people come to a bakery's site for, and the footer is on every
|
||||||
<p><a href="tel:+13097010660" class="hover:text-white transition">(309) 701-0660</a></p>
|
page. */-->
|
||||||
<p class="break-words">
|
<div class="mt-4 text-sm text-bakery-200"
|
||||||
<a href="mailto:[email protected]" class="hover:text-white transition">[email protected]</a>
|
th:replace="~{fragments/visit :: hours('text-sm text-bakery-200')}"></div>
|
||||||
</p>
|
|
||||||
</address>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<body>
|
||||||
|
<!--/*
|
||||||
|
The opening hours and the address, in one place.
|
||||||
|
|
||||||
|
They were on the homepage and in the footer already, and the moment the contact page wanted them too
|
||||||
|
that became three copies of the one fact a customer most often comes here for. A shop that changes its
|
||||||
|
Saturday hours should change them once.
|
||||||
|
|
||||||
|
Both fragments take the classes that vary by where they sit — cream text in the footer, sage on white
|
||||||
|
in a card — so the content is shared without the styling being averaged into something that suits
|
||||||
|
neither.
|
||||||
|
*/-->
|
||||||
|
|
||||||
|
<ul th:fragment="hours(rowClass)" class="space-y-3">
|
||||||
|
<li th:class="'flex justify-between gap-4 ' + ${rowClass}">
|
||||||
|
<span>Tuesday – Friday</span>
|
||||||
|
<span class="font-medium whitespace-nowrap">7:00am – 2:00pm</span>
|
||||||
|
</li>
|
||||||
|
<li th:class="'flex justify-between gap-4 ' + ${rowClass}">
|
||||||
|
<span>Saturday</span>
|
||||||
|
<span class="font-medium whitespace-nowrap">7:00am – 12:00pm</span>
|
||||||
|
</li>
|
||||||
|
<li th:class="'flex justify-between gap-4 ' + ${rowClass}">
|
||||||
|
<span>Sunday – Monday</span>
|
||||||
|
<span class="font-medium">Closed</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<address th:fragment="address(linkClass)" class="not-italic space-y-3">
|
||||||
|
<p>215 E Main Street<br>Princeville, IL 61559</p>
|
||||||
|
<p>
|
||||||
|
<a href="tel:+13097010660" th:class="${linkClass}">(309) 701-0660</a>
|
||||||
|
</p>
|
||||||
|
<p class="break-words">
|
||||||
|
<a href="mailto:[email protected]" th:class="${linkClass}">[email protected]</a>
|
||||||
|
</p>
|
||||||
|
</address>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,9 +3,24 @@
|
|||||||
<body>
|
<body>
|
||||||
<div th:fragment="content" class="bg-bakery-50">
|
<div th:fragment="content" class="bg-bakery-50">
|
||||||
|
|
||||||
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
<!--/* The shop's own front, which is what the story is about. Same band as the catering page. */-->
|
||||||
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Our story</h1>
|
<section class="relative flex items-center py-14 md:py-20 bg-bakery-900 text-white overflow-hidden">
|
||||||
</header>
|
<img th:src="${photos.of('gallery/Outside.webp')}" alt=""
|
||||||
|
class="absolute inset-0 w-full h-full object-cover object-bottom blur-[3px] scale-110">
|
||||||
|
<div class="absolute inset-0 bg-bakery-900/80"></div>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-bakery-900 via-bakery-800/60 to-bakery-900/80"></div>
|
||||||
|
<div class="relative container mx-auto px-4 w-full min-w-0">
|
||||||
|
<div class="max-w-3xl mx-auto text-center">
|
||||||
|
<p class="text-xs sm:text-sm uppercase tracking-[0.2em] text-bakery-200">Since 2024</p>
|
||||||
|
<h1 class="mt-3 font-adbhashitha text-4xl md:text-5xl text-bakery-50">Our story</h1>
|
||||||
|
<p class="mt-5 text-base sm:text-lg text-bakery-100 text-balance leading-relaxed">
|
||||||
|
215 E Main Street, in the middle of downtown Princeville.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<header class="container mx-auto px-4 pt-14 pb-2 md:pt-16 text-center"></header>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 pb-16 md:pb-24">
|
<div class="container mx-auto px-4 pb-16 md:pb-24">
|
||||||
<div class="max-w-2xl mx-auto space-y-12">
|
<div class="max-w-2xl mx-auto space-y-12">
|
||||||
@@ -49,6 +64,29 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--/* Somewhere to go after reading: the story is a preamble to an order. */-->
|
||||||
|
<section class="bg-bakery-800 text-white py-16 md:py-20">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="max-w-2xl mx-auto text-center">
|
||||||
|
<h2 class="font-adbhashitha text-2xl md:text-3xl">Come and see us</h2>
|
||||||
|
<p class="mt-4 text-bakery-100 leading-relaxed">
|
||||||
|
We open at 7:00am, Tuesday through Saturday. If you are after something for a date in the
|
||||||
|
diary, it is worth asking early.
|
||||||
|
</p>
|
||||||
|
<div class="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
<a href="/products"
|
||||||
|
class="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg">
|
||||||
|
See what we make
|
||||||
|
</a>
|
||||||
|
<a href="/contact"
|
||||||
|
class="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition">
|
||||||
|
Get in touch
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -69,6 +69,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!--/*
|
||||||
|
Ordering ahead. The catering page exists and the homepage said nothing about it — a customer only
|
||||||
|
found it by reading the nav. Photo on one side, the offer on the other, and the three tables named so
|
||||||
|
the link is worth following.
|
||||||
|
*/-->
|
||||||
|
<section class="py-16 md:py-24 bg-bakery-50">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="grid md:grid-cols-2 gap-8 md:gap-12 items-center max-w-5xl mx-auto">
|
||||||
|
<img th:src="${photos.of('gallery/Sugar Cookies.webp')}" alt="Iced sugar cookies from The Vine"
|
||||||
|
width="800" height="600" loading="lazy"
|
||||||
|
class="w-full h-64 md:h-80 object-cover rounded-3xl shadow-xs">
|
||||||
|
<div>
|
||||||
|
<h2 class="font-adbhashitha text-3xl md:text-4xl text-bakery-900">Ordering for a crowd?</h2>
|
||||||
|
<p class="mt-4 text-bakery-800 leading-relaxed">
|
||||||
|
Goodie boxes for the office, packages for a party, and cakes and desserts for a wedding —
|
||||||
|
with prices, sizes and what each one includes, so you can see where to start.
|
||||||
|
</p>
|
||||||
|
<a href="/catering"
|
||||||
|
class="mt-6 inline-block bg-bakery-600 hover:bg-bakery-700 text-white px-8 py-3.5 rounded-full font-medium tracking-wide transition">
|
||||||
|
Goodie boxes & catering
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!--/* Visit us */-->
|
<!--/* Visit us */-->
|
||||||
<section id="visit" class="py-16 md:py-24 bg-bakery-50 scroll-mt-24">
|
<section id="visit" class="py-16 md:py-24 bg-bakery-50 scroll-mt-24">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
@@ -76,32 +102,12 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 max-w-4xl mx-auto">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 max-w-4xl mx-auto">
|
||||||
<div class="bg-white rounded-3xl p-6 md:p-8">
|
<div class="bg-white rounded-3xl p-6 md:p-8">
|
||||||
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Our hours</h2>
|
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Our hours</h2>
|
||||||
<ul class="space-y-3 text-bakery-800">
|
<div th:replace="~{fragments/visit :: hours('text-bakery-800')}"></div>
|
||||||
<li class="flex justify-between gap-4">
|
|
||||||
<span>Tuesday – Friday</span>
|
|
||||||
<span class="font-medium whitespace-nowrap">7:00am – 2:00pm</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex justify-between gap-4">
|
|
||||||
<span>Saturday</span>
|
|
||||||
<span class="font-medium whitespace-nowrap">7:00am – 12:00pm</span>
|
|
||||||
</li>
|
|
||||||
<li class="flex justify-between gap-4">
|
|
||||||
<span>Sunday – Monday</span>
|
|
||||||
<span class="font-medium">Closed</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-3xl p-6 md:p-8">
|
<div class="bg-white rounded-3xl p-6 md:p-8">
|
||||||
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Find us</h2>
|
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Find us</h2>
|
||||||
<address class="not-italic space-y-3 text-bakery-800">
|
<div class="text-bakery-800"
|
||||||
<p>215 E Main Street<br>Princeville, IL 61559</p>
|
th:replace="~{fragments/visit :: address('hover:text-bakery-600 underline underline-offset-4')}"></div>
|
||||||
<p>
|
|
||||||
<a href="tel:+13097010660" class="hover:text-bakery-600 underline underline-offset-4">(309) 701-0660</a>
|
|
||||||
</p>
|
|
||||||
<p class="break-words">
|
|
||||||
<a href="mailto:[email protected]" class="hover:text-bakery-600 underline underline-offset-4">[email protected]</a>
|
|
||||||
</p>
|
|
||||||
</address>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
|
|
||||||
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
<header class="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
|
||||||
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Our products</h1>
|
<h1 class="font-adbhashitha text-4xl md:text-5xl text-bakery-900">Our products</h1>
|
||||||
|
<p class="mt-4 text-bakery-800 max-w-2xl mx-auto leading-relaxed">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 pb-16">
|
<div class="container mx-auto px-4 pb-16">
|
||||||
@@ -46,6 +50,29 @@
|
|||||||
<a href="/products" class="underline underline-offset-4">See everything</a>.
|
<a href="/products" class="underline underline-offset-4">See everything</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--/* A catalogue with no way to order from it is a gallery. */-->
|
||||||
|
<section class="bg-bakery-800 text-white py-16 md:py-20">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="max-w-2xl mx-auto text-center">
|
||||||
|
<h2 class="font-adbhashitha text-2xl md:text-3xl">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="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg">
|
||||||
|
Ask about an order
|
||||||
|
</a>
|
||||||
|
<a href="/catering"
|
||||||
|
class="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition">
|
||||||
|
Ordering for a crowd
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--/*
|
<!--/*
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Element defaults: the page background, the body type, the fade-in, and the two small resets the site
|
||||||
|
* needs. Anything with a class name belongs in components.css instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
/* Nothing on this site is meant to scroll sideways. */
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-bakery-50);
|
||||||
|
color: var(--color-bakery-900);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
overflow-x: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeIn 0.5s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html { scroll-behavior: auto; }
|
||||||
|
body { animation: none; opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--color-bakery-300);
|
||||||
|
color: var(--color-bakery-900);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* The public site's own classes — the ones that would be unreadable as a string of utilities in every
|
||||||
|
* template that needs them. The admin's controls are in admin.css.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* In @layer components, matching the old site. That ordering matters: a utility like `px-4` — which
|
||||||
|
the markup applies alongside `container` on nearly every section — has to win over this, or every
|
||||||
|
gutter on the site silently widens. */
|
||||||
|
@layer components {
|
||||||
|
.container {
|
||||||
|
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The two branch marks of the wordmark, as masks rather than inline SVG.
|
||||||
|
*
|
||||||
|
* They have to take their colour from the surrounding text — sage in the header, cream on the hero
|
||||||
|
* and in the footer — which an <img> cannot do. React solved that by inlining them through svgr,
|
||||||
|
* but these files are 31 KB each and the lockup appears up to three times on a page: inlining them
|
||||||
|
* in server-rendered HTML would add ~190 KB to every response. A mask over `currentColor` keeps the
|
||||||
|
* tinting, keeps the files cacheable, and costs two requests once.
|
||||||
|
*/
|
||||||
|
.mark {
|
||||||
|
background-color: currentColor;
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-position: center;
|
||||||
|
mask-position: center;
|
||||||
|
-webkit-mask-size: contain;
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
.mark-r {
|
||||||
|
-webkit-mask-image: url('/images/logo_R.svg');
|
||||||
|
mask-image: url('/images/logo_R.svg');
|
||||||
|
}
|
||||||
|
.mark-l {
|
||||||
|
-webkit-mask-image: url('/images/logo_L.svg');
|
||||||
|
mask-image: url('/images/logo_L.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The photo strip on a product card scrolls, but a scrollbar across the bottom of a photo is not part
|
||||||
|
* of the design. Hiding it is safe here because the strip is not the only way through the photos: it
|
||||||
|
* snaps, it takes arrow keys, and the arrows and dots are on top of it.
|
||||||
|
*/
|
||||||
|
.no-scrollbar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The mobile menu is a <details>; its default disclosure triangle would sit next to the hamburger. */
|
||||||
|
summary {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "itsthevine-styles",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tailwindcss -i site.css -o ../../../target/classes/static/css/site.css --minify",
|
||||||
|
"build:css": "tailwindcss -i site.css -o ../../../target/classes/static/css/site.css --minify",
|
||||||
|
"watch": "tailwindcss -i site.css -o ../../../target/classes/static/css/site.css --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/cli": "4.3.3",
|
||||||
|
"tailwindcss": "4.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* The site's one stylesheet, compiled by the Tailwind CLI into target/classes/static/css.
|
||||||
|
*
|
||||||
|
* It lives under src/main with the rest of the app's source, and it has to stay in the same directory as
|
||||||
|
* package.json: Tailwind resolves `@import "tailwindcss"` by walking up from the CSS file looking for
|
||||||
|
* node_modules. There is no bundler and no framework here — the site and the admin are both
|
||||||
|
* server-rendered HTML, and this is the whole of the asset pipeline.
|
||||||
|
*
|
||||||
|
* Imported in cascade order: names, then bare elements, then classes.
|
||||||
|
*/
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@import "./theme.css";
|
||||||
|
@import "./base.css";
|
||||||
|
@import "./components.css";
|
||||||
|
@import "./admin.css";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Where Tailwind looks for class names. Every template, and gallery.js — the product-card arrows are
|
||||||
|
* created in script, so their classes are only written down there. A utility exists in the output only if
|
||||||
|
* Tailwind saw it in one of these files, which is why a class name must never be assembled from pieces at
|
||||||
|
* runtime.
|
||||||
|
*/
|
||||||
|
@source "../resources/templates";
|
||||||
|
@source "../resources/static/js";
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* The brand: three typefaces and the sage-and-cream palette. Nothing here draws anything — these are the
|
||||||
|
* names the rest of the CSS and every template are written in.
|
||||||
|
*
|
||||||
|
* Fonts are served by Spring from /fonts rather than bundled, so the stylesheet can name a stable URL and
|
||||||
|
* the browser caches one copy across the whole site.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Raleway';
|
||||||
|
src: url('/fonts/raleway-latin.woff2') format('woff2');
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'AdBhashitha';
|
||||||
|
src: url('/fonts/AdBhashitha.woff') format('woff');
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'LeJour Script';
|
||||||
|
src: url('/fonts/LeJour-Script.woff') format('woff');
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
/* Sage & Cream. 500 is the signature sage; 600+ are the darker tones that white text can actually
|
||||||
|
sit on (500 on white is only 3.6:1 — too low). */
|
||||||
|
--color-bakery-50: #faf7f0;
|
||||||
|
--color-bakery-100: #f0efe3;
|
||||||
|
--color-bakery-200: #dde0cc;
|
||||||
|
--color-bakery-300: #c3cbae;
|
||||||
|
--color-bakery-400: #a2ae8b;
|
||||||
|
--color-bakery-500: #7c8b6b;
|
||||||
|
--color-bakery-600: #5f6f52;
|
||||||
|
--color-bakery-700: #4a5740;
|
||||||
|
--color-bakery-800: #37412f;
|
||||||
|
--color-bakery-900: #232b1e;
|
||||||
|
|
||||||
|
--font-sans: 'Raleway', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-adbhashitha: 'AdBhashitha', ui-serif, Georgia, serif;
|
||||||
|
--font-lejour: 'LeJour Script', ui-serif, Georgia, cursive;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "itsthevine-styles",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tailwindcss -i site.css -o ../target/classes/static/css/site.css --minify",
|
|
||||||
"build:css": "tailwindcss -i site.css -o ../target/classes/static/css/site.css --minify",
|
|
||||||
"watch": "tailwindcss -i site.css -o ../target/classes/static/css/site.css --watch"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tailwindcss/cli": "4.3.3",
|
|
||||||
"tailwindcss": "4.3.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* The stylesheet for the server-rendered site.
|
|
||||||
*
|
|
||||||
* Compiled by the Tailwind CLI (`npm run build:css`) straight into target/classes/static/css: it is a
|
|
||||||
* source file, not a resource, and the generated stylesheet belongs in the build output rather than in
|
|
||||||
* src/main/resources next to it.
|
|
||||||
*
|
|
||||||
* This directory is the whole asset pipeline, and Tailwind is all that is left of it: the site and the
|
|
||||||
* admin are both server-rendered HTML, so there is no bundler, no framework and one stylesheet. It has
|
|
||||||
* to live here because Tailwind resolves `@import "tailwindcss"` by walking up from the CSS file looking
|
|
||||||
* for node_modules — and node_modules is here.
|
|
||||||
*
|
|
||||||
* @source points Tailwind at every template (public pages and admin alike) and at gallery.js — the
|
|
||||||
* product-card arrows are created in script, so their classes are only written down there. A utility
|
|
||||||
* exists in the output only if Tailwind saw it in one of these files, which is why a class name must
|
|
||||||
* never be assembled from pieces at runtime.
|
|
||||||
*/
|
|
||||||
@import "tailwindcss";
|
|
||||||
@import "./tokens.css";
|
|
||||||
@import "./admin.css";
|
|
||||||
|
|
||||||
@source "../src/main/resources/templates";
|
|
||||||
@source "../src/main/resources/static/js";
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
* The brand: the sage-and-cream palette, the three fonts, and the base element rules.
|
|
||||||
*
|
|
||||||
* Shared deliberately. Two stylesheets are compiled from this — the server-rendered site
|
|
||||||
* (src/main/tailwind/site.css, scanning the Thymeleaf templates) and the admin SPA
|
|
||||||
* (frontend/src/index.css, scanning the React source) — and if the tokens were copied into both, the
|
|
||||||
* two halves of the same site would drift a shade apart the first time one of them was edited.
|
|
||||||
*
|
|
||||||
* Fonts are served by Spring from /fonts, not bundled by Vite, so both stylesheets can name the same
|
|
||||||
* URL and the browser caches one copy.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Raleway';
|
|
||||||
src: url('/fonts/raleway-latin.woff2') format('woff2');
|
|
||||||
font-weight: 100 900;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'AdBhashitha';
|
|
||||||
src: url('/fonts/AdBhashitha.woff') format('woff');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'LeJour Script';
|
|
||||||
src: url('/fonts/LeJour-Script.woff') format('woff');
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
/* Sage & Cream. 500 is the signature sage; 600+ are the darker tones that white text can actually
|
|
||||||
sit on (500 on white is only 3.6:1 — too low). */
|
|
||||||
--color-bakery-50: #faf7f0;
|
|
||||||
--color-bakery-100: #f0efe3;
|
|
||||||
--color-bakery-200: #dde0cc;
|
|
||||||
--color-bakery-300: #c3cbae;
|
|
||||||
--color-bakery-400: #a2ae8b;
|
|
||||||
--color-bakery-500: #7c8b6b;
|
|
||||||
--color-bakery-600: #5f6f52;
|
|
||||||
--color-bakery-700: #4a5740;
|
|
||||||
--color-bakery-800: #37412f;
|
|
||||||
--color-bakery-900: #232b1e;
|
|
||||||
|
|
||||||
--font-sans: 'Raleway', ui-sans-serif, system-ui, sans-serif;
|
|
||||||
--font-adbhashitha: 'AdBhashitha', ui-serif, Georgia, serif;
|
|
||||||
--font-lejour: 'LeJour Script', ui-serif, Georgia, cursive;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
/* Nothing on this site is meant to scroll sideways. */
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--color-bakery-50);
|
|
||||||
color: var(--color-bakery-900);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
overflow-x: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
animation: fadeIn 0.5s ease-in forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
html { scroll-behavior: auto; }
|
|
||||||
body { animation: none; opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* In @layer components, matching the old site. That ordering matters: a utility like `px-4` — which
|
|
||||||
the markup applies alongside `container` on nearly every section — has to win over this, or every
|
|
||||||
gutter on the site silently widens. */
|
|
||||||
@layer components {
|
|
||||||
.container {
|
|
||||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The two branch marks of the wordmark, as masks rather than inline SVG.
|
|
||||||
*
|
|
||||||
* They have to take their colour from the surrounding text — sage in the header, cream on the hero
|
|
||||||
* and in the footer — which an <img> cannot do. React solved that by inlining them through svgr,
|
|
||||||
* but these files are 31 KB each and the lockup appears up to three times on a page: inlining them
|
|
||||||
* in server-rendered HTML would add ~190 KB to every response. A mask over `currentColor` keeps the
|
|
||||||
* tinting, keeps the files cacheable, and costs two requests once.
|
|
||||||
*/
|
|
||||||
.mark {
|
|
||||||
background-color: currentColor;
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
-webkit-mask-position: center;
|
|
||||||
mask-position: center;
|
|
||||||
-webkit-mask-size: contain;
|
|
||||||
mask-size: contain;
|
|
||||||
}
|
|
||||||
.mark-r {
|
|
||||||
-webkit-mask-image: url('/images/logo_R.svg');
|
|
||||||
mask-image: url('/images/logo_R.svg');
|
|
||||||
}
|
|
||||||
.mark-l {
|
|
||||||
-webkit-mask-image: url('/images/logo_L.svg');
|
|
||||||
mask-image: url('/images/logo_L.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The photo strip on a product card scrolls, but a scrollbar across the bottom of a photo is not part
|
|
||||||
* of the design. Hiding it is safe here because the strip is not the only way through the photos: it
|
|
||||||
* snaps, it takes arrow keys, and the arrows and dots are on top of it.
|
|
||||||
*/
|
|
||||||
.no-scrollbar {
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
.no-scrollbar::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The mobile menu is a <details>; its default disclosure triangle would sit next to the hamburger. */
|
|
||||||
summary {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background-color: var(--color-bakery-300);
|
|
||||||
color: var(--color-bakery-900);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user