Rewrite on the Bennett platform: Spring Boot + Vite/React SPA
Replaces the Next.js app. Same site, same look; the parts that were decisions rather than markup now live in Java. - catalogue, curated order, category filter and image URLs move from a TypeScript array into Postgres behind /api/products and /api/categories - contact form uses the shared platform-starter-contact: validate, RECORD, send, then fan out to n8n. Recording first means a relay outage costs a notification, not an enquiry - PageMetaController rewrites title/description/OG per route, replacing what Next's SSR gave crawlers and link-preview scrapers - 50MB of photos leave the repo for the MinIO bucket, re-encoded to webp (14MB) with EXIF (including phone GPS) stripped - fixes a catalogue typo: 'Strawberry Pie' was category 'Pies', which no filter matched, so it was unreachable unless browsing All
@@ -1,10 +1,7 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
npm-debug.log
|
||||
.env
|
||||
.env.local
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
default.nix
|
||||
target/
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
.git/
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# SMTP credentials for the contact form. Copy to .env and fill in.
|
||||
# .env is gitignored — never commit real values.
|
||||
SMTP_SERVER=
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=
|
||||
SMTP_TOKEN=
|
||||
|
||||
# Where contact form submissions are delivered.
|
||||
CONTACT_TO=[email protected]
|
||||
# Envelope sender. Many SMTP providers require this to match SMTP_USERNAME or a verified domain.
|
||||
CONTACT_FROM=
|
||||
|
||||
# Optional: automation hub (n8n) webhook. When set, each enquiry is also POSTed here
|
||||
# (fire-and-forget) so the hub can send the customer auto-reply and create CRM/task
|
||||
# records. The direct email above stays the reliable path; leave blank to disable.
|
||||
CONTACT_HUB_URL=
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
<servers>
|
||||
<server>
|
||||
<id>gitea</id>
|
||||
<username>${env.MAVEN_USER}</username>
|
||||
<password>${env.MAVEN_TOKEN}</password>
|
||||
</server>
|
||||
</servers>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>gitea</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>gitea</id>
|
||||
<url>https://git.thebennett.net/api/packages/austin/maven</url>
|
||||
<releases><enabled>true</enabled></releases>
|
||||
<snapshots><enabled>true</enabled></snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
<activeProfiles><activeProfile>gitea</activeProfile></activeProfiles>
|
||||
</settings>
|
||||
@@ -0,0 +1,49 @@
|
||||
name: build-and-publish
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# Files that can't change the image. Skipping them avoids a pointless rebuild that Watchtower
|
||||
# would then redeploy — a few seconds of downtime on a live site for a docs-only commit.
|
||||
paths-ignore: ["renovate.json", "**.md"]
|
||||
# Lets `rebuild-all-apps.sh` force a rebuild (e.g. to roll out an urgent platform fix immediately
|
||||
# instead of waiting for a Renovate bump PR).
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the Gitea container registry
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.thebennett.net -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
# The platform is now referenced by an immutable RELEASE version, so a cached maven layer can't
|
||||
# silently hold an old build — layer caching is safe again (and much faster).
|
||||
- name: Build image
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
MAVEN_USER: ${{ secrets.REGISTRY_USER }}
|
||||
MAVEN_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
docker build \
|
||||
--secret id=maven_user,env=MAVEN_USER \
|
||||
--secret id=maven_token,env=MAVEN_TOKEN \
|
||||
--build-arg GIT_SHA=${{ github.sha }} \
|
||||
-t git.thebennett.net/thevine/itsthevine:latest \
|
||||
-t git.thebennett.net/thevine/itsthevine:${{ github.sha }} .
|
||||
|
||||
- name: Scan image (Trivy)
|
||||
run: |
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
aquasec/trivy:latest image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --no-progress \
|
||||
git.thebennett.net/thevine/itsthevine:latest || true
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
aquasec/trivy:latest image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
--pkg-types library --exit-code 1 --no-progress \
|
||||
git.thebennett.net/thevine/itsthevine:latest
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push git.thebennett.net/thevine/itsthevine:latest
|
||||
docker push git.thebennett.net/thevine/itsthevine:${{ github.sha }}
|
||||
@@ -1,25 +0,0 @@
|
||||
name: build-and-publish
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the Gitea container registry
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.thebennett.net -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
docker build \
|
||||
-t git.thebennett.net/thevine/itsthevine:latest \
|
||||
-t git.thebennett.net/thevine/itsthevine:${{ github.sha }} .
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push git.thebennett.net/thevine/itsthevine:latest
|
||||
docker push git.thebennett.net/thevine/itsthevine:${{ github.sha }}
|
||||
@@ -1,41 +1,7 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
target/
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for commiting if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Multi-stage build for the Next.js (standalone) itsthevine.com site.
|
||||
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
# framer-motion@11 declares a React 18 peer; project is on React 19 (as built on Vercel)
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
# ---------- build ----------
|
||||
FROM maven:3.9-eclipse-temurin-25 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
# Resolve the platform from the Gitea Maven registry (creds via BuildKit secrets); the Maven build also
|
||||
# runs the Vite/React SPA build (frontend-maven-plugin) and folds it into the jar. Tests are skipped here
|
||||
# because Testcontainers needs a Docker daemon — they run via `mvn verify`, not in the image build.
|
||||
RUN --mount=type=secret,id=maven_user --mount=type=secret,id=maven_token \
|
||||
MAVEN_USER="$(cat /run/secrets/maven_user)" MAVEN_TOKEN="$(cat /run/secrets/maven_token)" \
|
||||
mvn -B -ntp -s .gitea/ci-settings.xml -DskipTests package \
|
||||
&& cp "$(ls target/itsthevine-*.jar | grep -v original | head -1)" app.jar \
|
||||
&& java -Djarmode=tools -jar app.jar extract --layers --destination extracted
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
# ---------- runtime ----------
|
||||
FROM eclipse-temurin:25-jre-alpine AS runtime
|
||||
RUN apk -U upgrade --no-cache && apk add --no-cache curl
|
||||
RUN addgroup -S spring && adduser -S -D -H -h /app -s /sbin/nologin -G spring spring
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
PORT=3000 \
|
||||
HOSTNAME=0.0.0.0
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
|
||||
# standalone output already contains a minimal node_modules + server.js
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
COPY --from=build --chown=spring:spring /src/extracted/dependencies/ ./
|
||||
COPY --from=build --chown=spring:spring /src/extracted/snapshot-dependencies/ ./
|
||||
COPY --from=build --chown=spring:spring /src/extracted/application/ ./
|
||||
|
||||
USER spring
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD curl -fsS http://localhost:8080/actuator/health || exit 1
|
||||
|
||||
ARG GIT_SHA=unknown
|
||||
LABEL org.opencontainers.image.title="itsthevine" \
|
||||
org.opencontainers.image.source="https://git.thebennett.net/thevine/itsthevine" \
|
||||
org.opencontainers.image.revision="${GIT_SHA}"
|
||||
|
||||
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
|
||||
|
||||
@@ -1,11 +1,69 @@
|
||||
# [The Vine Coffeehouse & Bakery](http://itsthevine.com)
|
||||
# The Vine Coffeehouse + Bakery — itsthevine.com
|
||||
|
||||
## Getting Started
|
||||
Site for The Vine, 215 E Main Street, Princeville, Illinois. Spring Boot serving a Vite/React SPA,
|
||||
on [the Bennett platform](https://git.thebennett.net/austin/platform).
|
||||
|
||||
First, run the development server:
|
||||
Previously a Next.js app on Cloudflare, then self-hosted; the look is unchanged.
|
||||
|
||||
## Shape
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Backend | Spring Boot 4 / Java 25, `com.itsthevine.web` |
|
||||
| Frontend | Vite + React 19 + TypeScript + Tailwind v4, served from the jar |
|
||||
| Database | Postgres (`itsthevine` on the shared `app-db` cluster), Flyway |
|
||||
| Photos | public MinIO bucket `itsthevine` — **not** in the repo or the image |
|
||||
| Deploy | Gitea CI → image → Watchtower → Caddy |
|
||||
|
||||
## What the server owns
|
||||
|
||||
The SPA renders; it doesn't decide anything.
|
||||
|
||||
- **`/api/products`**, **`/api/categories`** — the catalogue, its curated order, the category filter
|
||||
and the absolute image URLs. This was a TypeScript array shipped to every visitor; it's now a table
|
||||
(`V2__products.sql`) read through `ProductCatalog`.
|
||||
- **`/api/contact`** — validates, **records the enquiry**, emails it, then fans out to the n8n hub.
|
||||
Recorded before sending on purpose: a relay outage costs a notification, not the enquiry. Undelivered
|
||||
ones are `enquiry.delivered = false`. Validation and delivery come from `platform-starter-contact`,
|
||||
shared with the other sites.
|
||||
- **Per-page metadata** — `PageMetaController` rewrites `<title>`/`<meta>`/OG tags per route. Next used
|
||||
to server-render these; a plain SPA would hand crawlers and link-preview scrapers one generic shell.
|
||||
|
||||
## Photos
|
||||
|
||||
Re-encoded to webp and uploaded to the bucket once (50 MB of originals → 14 MB), served with a
|
||||
year-long cache. `site.assets.base-url` says where they live. The originals remain in this repo's
|
||||
history. EXIF (including GPS from phone photos) is stripped by the re-encode.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# backend (needs Postgres on :5432 with an itsthevine database)
|
||||
mvn spring-boot:run
|
||||
|
||||
# frontend, proxies /api to :8080
|
||||
cd frontend && npm install && npm run dev # http://localhost:2024
|
||||
```
|
||||
|
||||
Open http://localhost:3000 with your browser to see the result.
|
||||
Build without the SPA for quick backend loops: `mvn -DskipFrontend=true package`.
|
||||
|
||||
Tests need Docker (Testcontainers):
|
||||
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `DB_URL` / `DB_USER` / `DB_PASSWORD` | Postgres |
|
||||
| `SMTP_SERVER` / `SMTP_PORT` / `SMTP_USERNAME` / `SMTP_TOKEN` | relay for the contact form |
|
||||
| `CONTACT_TO` / `CONTACT_FROM` | enquiry recipient and envelope sender |
|
||||
| `CONTACT_HUB_URL` | optional n8n webhook; best-effort, never blocks a submission |
|
||||
| `SITE_BASE_URL` | absolute base for `og:url` |
|
||||
| `VITE_ASSET_BASE` / `site.assets.base-url` | photo bucket |
|
||||
|
||||
`platform.contact.to` unset disables the contact starter, and the app won't start with a
|
||||
`ContactController` that has nothing to send with — so a missing `CONTACT_TO` fails loudly rather than
|
||||
silently dropping enquiries.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
let
|
||||
pkgs = import <nixpkgs> {};
|
||||
in
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.nodejs
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
if ! [ -d "node_modules" ]; then
|
||||
npm install --legacy-peer-deps
|
||||
fi
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" media="(prefers-color-scheme: light)" href="/images/resources/logo_L.png">
|
||||
<link rel="icon" media="(prefers-color-scheme: dark)" href="/images/resources/logo_dark.png">
|
||||
<!-- Preconnect to the photo bucket so product images start loading a round-trip sooner. -->
|
||||
<link rel="preconnect" href="https://s3.thebennett.net" crossorigin>
|
||||
<!-- PageMetaController rewrites the title and the four meta tags below per route, so crawlers and
|
||||
link-preview scrapers get real per-page metadata instead of one generic shell. Keep the
|
||||
attribute order and quoting as-is — it matches on them. -->
|
||||
<title>The Vine Coffeehouse + Bakery</title>
|
||||
<meta name="description" content="A locally owned coffeehouse and bakery in downtown Princeville, Illinois. We bake pastries, custom cakes, cookies, and cinnamon rolls, and serve sandwiches, paninis, and coffee.">
|
||||
<meta property="og:title" content="The Vine Coffeehouse + Bakery">
|
||||
<meta property="og:description" content="A locally owned coffeehouse and bakery in downtown Princeville, IL.">
|
||||
<meta property="og:url" content="https://itsthevine.com">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta name="keywords" content="bakery, coffeehouse, pastries, custom cakes, cinnamon rolls, paninis, Princeville IL">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "itsthevine-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "12.34.4",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-router-dom": "7.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.3.3",
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"tailwindcss": "4.3.3",
|
||||
"typescript": "^5.9.0",
|
||||
"vite": "^8.1.3",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
@@ -0,0 +1,40 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import HomePage from '@/pages/Home';
|
||||
import ProductsPage from '@/pages/Products';
|
||||
import HistoryPage from '@/pages/History';
|
||||
import ContactPage from '@/pages/Contact';
|
||||
import NotFoundPage from '@/pages/NotFound';
|
||||
|
||||
/**
|
||||
* Client-side navigation keeps the previous scroll position, which lands you halfway down a page you
|
||||
* just opened. Anchors like /#visit still need to work, so only reset when there isn't one.
|
||||
*/
|
||||
const ScrollToTop = () => {
|
||||
const { pathname, hash } = useLocation();
|
||||
useEffect(() => {
|
||||
if (!hash) window.scrollTo(0, 0);
|
||||
}, [pathname, hash]);
|
||||
return null;
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<div className="min-h-screen bg-bakery-50 flex flex-col">
|
||||
<ScrollToTop />
|
||||
<Header />
|
||||
<main className="flex-grow">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/products" element={<ProductsPage />} />
|
||||
<Route path="/history" element={<HistoryPage />} />
|
||||
<Route path="/contact" element={<ContactPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@@ -1,4 +1,4 @@
|
||||
import Link from 'next/link';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Logo from './Logo';
|
||||
|
||||
const Footer = () => {
|
||||
@@ -16,13 +16,13 @@ const Footer = () => {
|
||||
<h3 className="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Navigation</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link href="/products" className="hover:text-white transition">Our Products</Link>
|
||||
<Link to="/products" className="hover:text-white transition">Our Products</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/history" className="hover:text-white transition">Our Story</Link>
|
||||
<Link to="/history" className="hover:text-white transition">Our Story</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contact" className="hover:text-white transition">Contact</Link>
|
||||
<Link to="/contact" className="hover:text-white transition">Contact</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Logo from './Logo';
|
||||
|
||||
const Header = () => {
|
||||
@@ -24,7 +23,7 @@ const Header = () => {
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
to={item.href}
|
||||
className="text-sm uppercase tracking-[0.15em] text-bakery-700 hover:text-bakery-900 transition"
|
||||
>
|
||||
{item.label}
|
||||
@@ -87,7 +86,7 @@ const Header = () => {
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
to={item.href}
|
||||
className="text-bakery-800 hover:text-bakery-600 transition py-4 text-lg"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import Logo_R from 'public/images/resources/logo_R.svg';
|
||||
import Logo_L from 'public/images/resources/logo_L.svg';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Logo_R from '@/assets/logo_R.svg?react';
|
||||
import Logo_L from '@/assets/logo_L.svg?react';
|
||||
|
||||
interface LogoProps {
|
||||
/** Tailwind text-* class. The SVG marks fill with currentColor, so this colors the whole lockup. */
|
||||
@@ -55,7 +55,7 @@ const Logo: React.FC<LogoProps> = ({ className = 'text-bakery-700', size = 'sm',
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href="/" className={classes} aria-label="The Vine Coffeehouse + Bakery, home">
|
||||
<Link to="/" className={classes} aria-label="The Vine Coffeehouse + Bakery, home">
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ProductGalleryProps {
|
||||
images: string[];
|
||||
alt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The square photo on a product card, with arrows when there's more than one shot.
|
||||
*
|
||||
* Replaces react-awesome-slider, which hasn't been published since 2020 and pins peer deps to
|
||||
* React 16 — the same job in a fraction of the code, and one less unmaintained dependency in a
|
||||
* build we gate on CVEs. Behaviour is what the old cards did: one image at a time, square crop,
|
||||
* arrows only when they'd do something.
|
||||
*/
|
||||
const ProductGallery: React.FC<ProductGalleryProps> = ({ images, alt }) => {
|
||||
const [index, setIndex] = useState(0);
|
||||
const many = images.length > 1;
|
||||
|
||||
const step = (delta: number) => setIndex((i) => (i + delta + images.length) % images.length);
|
||||
|
||||
return (
|
||||
<div className="relative aspect-square bg-bakery-100">
|
||||
{images.map((src, i) => (
|
||||
<img
|
||||
key={src}
|
||||
src={src}
|
||||
// Only the visible frame gets described; the rest are decorative duplicates of the same item.
|
||||
alt={i === 0 ? alt : ''}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${
|
||||
i === index ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
aria-hidden={i === index ? undefined : true}
|
||||
/>
|
||||
))}
|
||||
|
||||
{many && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => step(-1)}
|
||||
aria-label={`Previous photo of ${alt}`}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 grid place-items-center h-10 w-10 rounded-full bg-bakery-900/40 text-white text-2xl leading-none backdrop-blur-sm transition hover:bg-bakery-900/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||||
>
|
||||
<span aria-hidden="true">{'<'}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => step(1)}
|
||||
aria-label={`Next photo of ${alt}`}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 grid place-items-center h-10 w-10 rounded-full bg-bakery-900/40 text-white text-2xl leading-none backdrop-blur-sm transition hover:bg-bakery-900/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||||
>
|
||||
<span aria-hidden="true">{'>'}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductGallery;
|
||||
@@ -0,0 +1,90 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Brand fonts ship with the app rather than coming from Google — same look, no third-party request
|
||||
on every page load. Raleway is the variable latin subset. */
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Deliberately unlayered so it overrides Tailwind's own `container` utility — the site's gutters are
|
||||
part of the layout, not the breakpoint default. */
|
||||
.container {
|
||||
max-width: 80rem;
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
@media (min-width: 40rem) {
|
||||
.container { padding-inline: 1.5rem; }
|
||||
}
|
||||
@media (min-width: 64rem) {
|
||||
.container { padding-inline: 2rem; }
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--color-bakery-300);
|
||||
color: var(--color-bakery-900);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* The catalogue, its ordering, its category filter and its image URLs are all decided by the
|
||||
* backend — this file just fetches them. Same origin, so no base URL and no CORS.
|
||||
*/
|
||||
|
||||
export interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string;
|
||||
/** Absolute, ready to put in a src. Built server-side from the bucket config. */
|
||||
images: string[];
|
||||
}
|
||||
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const res = await fetch(path, { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error(`${path} responded ${res.status}`);
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const fetchProducts = (category?: string) =>
|
||||
get<Product[]>(category && category !== 'All' ? `/api/products?category=${encodeURIComponent(category)}` : '/api/products');
|
||||
|
||||
export const fetchCategories = () => get<string[]>('/api/categories');
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Photos live in the public MinIO bucket, not in the app image — 50 MB of JPEGs has no business
|
||||
* inside a container we redeploy on every commit, and the bucket serves them with a year-long
|
||||
* cache. Small brand assets (logo marks, favicons) stay local so first paint needs nothing external.
|
||||
*/
|
||||
const BASE = (import.meta.env.VITE_ASSET_BASE ?? 'https://s3.thebennett.net/itsthevine').replace(/\/$/, '');
|
||||
|
||||
/**
|
||||
* `photo('products/scones.webp')` -> absolute bucket URL.
|
||||
*
|
||||
* Segments are encoded individually: some gallery files have spaces in their names ("Cinnamon
|
||||
* Rolls.webp") and a raw space in a URL doesn't fetch.
|
||||
*/
|
||||
export function photo(key: string): string {
|
||||
const path = key.replace(/^\//, '').split('/').map(encodeURIComponent).join('/');
|
||||
return `${BASE}/images/${path}`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,4 +1,3 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Status = 'idle' | 'sending' | 'sent' | 'error';
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import Logo from './Logo';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Logo from '@/components/Logo';
|
||||
import { photo } from '@/lib/assets';
|
||||
|
||||
const HomePage = () => {
|
||||
return (
|
||||
@@ -11,13 +10,11 @@ const HomePage = () => {
|
||||
{/* Softened on purpose: the storefront's own painted sign sits right behind
|
||||
the logo, so a sharp photo makes you read the name twice. scale-110 hides
|
||||
the blur's feathered edges. */}
|
||||
<Image
|
||||
src="/images/gallery/Outside.jpg"
|
||||
<img
|
||||
src={photo('gallery/Outside.webp')}
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
quality={90}
|
||||
className="object-cover object-bottom blur-[3px] scale-110"
|
||||
fetchPriority="high"
|
||||
className="absolute inset-0 w-full h-full object-cover object-bottom blur-[3px] scale-110"
|
||||
/>
|
||||
{/* Sage wash rather than a neutral black scrim — the tint is the identity. */}
|
||||
<div className="absolute inset-0 bg-bakery-900/80" />
|
||||
@@ -33,7 +30,7 @@ const HomePage = () => {
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Link
|
||||
href="/products"
|
||||
to="/products"
|
||||
className="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 the menu
|
||||
@@ -49,8 +46,6 @@ const HomePage = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Our Specialties */}
|
||||
<section className="py-16 md:py-24 bg-bakery-50">
|
||||
<div className="container mx-auto px-4">
|
||||
@@ -62,12 +57,12 @@ const HomePage = () => {
|
||||
className="group text-center bg-white rounded-3xl overflow-hidden shadow-sm hover:shadow-lg hover:-translate-y-1 transition duration-300"
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<Image
|
||||
src={`/images/gallery/${item}.png`}
|
||||
<img
|
||||
src={photo(`gallery/${item}.webp`)}
|
||||
alt={item}
|
||||
width={600}
|
||||
height={400}
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
|
||||
loading="lazy"
|
||||
className="w-full h-56 object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
@@ -87,7 +82,7 @@ const HomePage = () => {
|
||||
Morissa Bennett opened The Vine in 2024. We bake in our own kitchen on Main Street:
|
||||
cinnamon rolls, cookies, custom cakes, sandwiches, paninis, and coffee.
|
||||
</p>
|
||||
<Link href="/history" className="text-white hover:text-bakery-200 font-semibold underline underline-offset-4">
|
||||
<Link to="/history" className="text-white hover:text-bakery-200 font-semibold underline underline-offset-4">
|
||||
Read our story
|
||||
</Link>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<div className="bg-bakery-50">
|
||||
<div className="container mx-auto px-4 py-24 md:py-32 text-center">
|
||||
<h1 className="font-adbhashitha text-4xl md:text-5xl text-bakery-900 mb-6">
|
||||
We could not find that page
|
||||
</h1>
|
||||
<p className="text-bakery-800 mb-10">
|
||||
It may have moved. The menu, our story, and how to reach us are all still here.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Link
|
||||
to="/"
|
||||
className="bg-bakery-600 hover:bg-bakery-700 text-white px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"
|
||||
>
|
||||
Back home
|
||||
</Link>
|
||||
<Link
|
||||
to="/products"
|
||||
className="border border-bakery-300 hover:bg-bakery-100 text-bakery-700 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"
|
||||
>
|
||||
See the menu
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
@@ -1,16 +1,28 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { products, categories } from '@/data/products';
|
||||
import 'react-awesome-slider/dist/styles.css';
|
||||
import AwesomeSlider from 'react-awesome-slider';
|
||||
import { fetchCategories, fetchProducts, type Product } from '@/lib/api';
|
||||
import ProductGallery from '@/components/ProductGallery';
|
||||
|
||||
const ProductsPage = () => {
|
||||
const [categories, setCategories] = useState<string[]>(['All']);
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
const filteredProducts = selectedCategory === 'All'
|
||||
? products
|
||||
: products.filter(product => product.category === selectedCategory);
|
||||
useEffect(() => {
|
||||
fetchCategories().then(setCategories).catch(() => setFailed(true));
|
||||
}, []);
|
||||
|
||||
// The filter is applied by the API, not in the browser — one source of truth for what's in a
|
||||
// category. `ignore` drops a slow response that lost the race to a newer click.
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
setFailed(false);
|
||||
fetchProducts(selectedCategory)
|
||||
.then((p) => { if (!ignore) setProducts(p); })
|
||||
.catch(() => { if (!ignore) setFailed(true); });
|
||||
return () => { ignore = true; };
|
||||
}, [selectedCategory]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bakery-50">
|
||||
@@ -42,13 +54,20 @@ const ProductsPage = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{failed && (
|
||||
<p role="alert" className="text-center text-bakery-800">
|
||||
We could not load the menu just now. Please refresh, or call us on{' '}
|
||||
<a href="tel:+13097010660" className="underline underline-offset-4">(309) 701-0660</a>.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Products Grid */}
|
||||
<motion.div
|
||||
layout
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{filteredProducts.map((product) => (
|
||||
{products.map((product) => (
|
||||
<motion.div
|
||||
key={product.id}
|
||||
layout
|
||||
@@ -59,18 +78,7 @@ const ProductsPage = () => {
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<AwesomeSlider
|
||||
bullets={false}
|
||||
style={{ aspectRatio: '1 / 1' }}
|
||||
organicArrows={product.images.length > 1}
|
||||
customContent={true}
|
||||
buttonContentLeft={product.images.length > 1 ? <span className="text-white text-2xl">{'<'}</span> : null}
|
||||
buttonContentRight={product.images.length > 1 ? <span className="text-white text-2xl">{'>'}</span> : null}
|
||||
>
|
||||
{product.images.map((image, index) => (
|
||||
<div key={index} data-src={image} />
|
||||
))}
|
||||
</AwesomeSlider>
|
||||
<ProductGallery images={product.images} alt={product.name} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["./src/*"] }
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import path from 'node:path';
|
||||
|
||||
export default defineConfig({
|
||||
// svgr keeps the `import Logo from './x.svg?react'` style the old @svgr/webpack setup used, so the
|
||||
// logo marks stay inline SVG and inherit currentColor.
|
||||
plugins: [react(), tailwindcss(), svgr()],
|
||||
resolve: {
|
||||
alias: { '@': path.resolve(__dirname, './src') },
|
||||
},
|
||||
server: {
|
||||
port: 2024,
|
||||
// Backend on :8080 during development; the built SPA is served by Spring, same origin.
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8080',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
// The photos live in MinIO, so what's left is small — a warning here would mean a real regression.
|
||||
chunkSizeWarningLimit: 600,
|
||||
},
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone', // self-contained server bundle for the Docker image
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'itsthevine.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
turbopack: {
|
||||
rules: {
|
||||
'*.svg': {
|
||||
loaders: ['@svgr/webpack'],
|
||||
as: '*.js',
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "itsthevine.com",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2024",
|
||||
"build": "next build",
|
||||
"start": "next start -p 2024",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"framer-motion": "^11.11.11",
|
||||
"next": "^16.2.10",
|
||||
"nodemailer": "^9.0.3",
|
||||
"react": "^19.0.0",
|
||||
"react-awesome-slider": "^4.1.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sass": "^1.83.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-next": "15.0.2",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>net.thebennett.platform</groupId>
|
||||
<artifactId>platform-parent</artifactId>
|
||||
<version>0.1.4</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.itsthevine</groupId>
|
||||
<artifactId>itsthevine</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<name>The Vine Coffeehouse + Bakery</name>
|
||||
<description>Site for The Vine in Princeville, IL — menu, story, and contact form — on the Bennett platform.</description>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.thebennett.platform</groupId>
|
||||
<artifactId>platform-bom</artifactId>
|
||||
<version>0.1.4</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- Spring Boot 4.1 no longer manages the raw org.testcontainers:* module versions -->
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>1.21.4</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<!-- Platform releases live in the Gitea Maven registry (anonymous read). Declared here so Renovate
|
||||
can discover new platform versions and open a bump PR. Maven still needs this repo in
|
||||
settings.xml for PARENT resolution (see .gitea/ci-settings.xml). -->
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>gitea</id>
|
||||
<url>https://git.thebennett.net/api/packages/austin/maven</url>
|
||||
<releases><enabled>true</enabled></releases>
|
||||
<snapshots><enabled>false</enabled></snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.thebennett.platform</groupId>
|
||||
<artifactId>platform-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.thebennett.platform</groupId>
|
||||
<artifactId>platform-starter-data</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.thebennett.platform</groupId>
|
||||
<artifactId>platform-starter-contact</artifactId>
|
||||
</dependency>
|
||||
<!-- No starter-security: this is a public brochure site with nothing to sign in to, and no
|
||||
starter-storage: the photos are served straight from the public MinIO bucket. -->
|
||||
|
||||
<!-- test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-testcontainers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<!-- SPA build inherited from platform-parent (node install + npm build + copy dist -> jar). -->
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 468 KiB |
|
Before Width: | Height: | Size: 473 KiB |
|
Before Width: | Height: | Size: 509 KiB |
|
Before Width: | Height: | Size: 917 KiB |
|
Before Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 454 KiB |
|
Before Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 628 KiB |
|
Before Width: | Height: | Size: 442 KiB |
|
Before Width: | Height: | Size: 643 KiB |
|
Before Width: | Height: | Size: 590 KiB |
|
Before Width: | Height: | Size: 680 KiB |
|
Before Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 740 KiB |
|
Before Width: | Height: | Size: 498 KiB |
|
Before Width: | Height: | Size: 645 KiB |
|
Before Width: | Height: | Size: 511 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 454 KiB |
|
Before Width: | Height: | Size: 490 KiB |
|
Before Width: | Height: | Size: 842 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 630 KiB |
|
Before Width: | Height: | Size: 630 KiB |
|
Before Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 592 KiB |
|
Before Width: | Height: | Size: 667 KiB |
|
Before Width: | Height: | Size: 670 KiB |
|
Before Width: | Height: | Size: 419 KiB |
|
Before Width: | Height: | Size: 444 KiB |
|
Before Width: | Height: | Size: 355 KiB |
|
Before Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 496 KiB |
|
Before Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 422 KiB |
|
Before Width: | Height: | Size: 422 KiB |
|
Before Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 350 KiB |
|
Before Width: | Height: | Size: 459 KiB |
|
Before Width: | Height: | Size: 424 KiB |
|
Before Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 449 KiB |
|
Before Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 503 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 636 KiB |
|
Before Width: | Height: | Size: 696 KiB |
|
Before Width: | Height: | Size: 639 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 296 KiB |