Replace the front page with an admin-driven home

The home page is rebuilt from scratch and configured from a new
Front page tab in the admin, backed by migration 017 and served by
GET /api/front-page.

Hero: brand, photos (crossfading slideshow with progress and pause)
or livestream (YouTube/Facebook/Vimeo embed with a LIVE badge),
switched by hand. After it, bands the admin can reorder, retitle or
hide: a countdown to the next event (series-aware), the National
Retreats carousel, a numbers band (typed in or counted from the
database), a horizontal rail of featured timeline entries, and a
"Find your way in" pathfinder replacing the old connect section.

The CRUD engine gains a `singleton` flag: the entity has one row,
made by its migration, and create and delete are refused. The list
screen opens that row and the editor drops the slug, back link and
delete. shapeSeries moves to shape.js so /front-page can share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-25 05:37:42 -05:00
parent b4b013209b
commit 6a69084de2
25 changed files with 2299 additions and 494 deletions

View file

@ -0,0 +1,45 @@
/* ═══════════════════════════════════════════════════════════════
HOME LINK
Every link on the front page is typed into the admin, so any of
them can be a route (/retreats), an anchor on this page
(#connect) or somewhere else entirely. One component decides
which element that is, so the hero buttons, photo captions and
pathfinder actions can't disagree about it.
═══════════════════════════════════════════════════════════════ */
import type { CSSProperties, ReactNode } from 'react'
import { Link } from 'react-router-dom'
type HomeLinkProps = {
url: string
className?: string
style?: CSSProperties
children: ReactNode
}
export const isExternal = (url: string) => /^[a-z][a-z0-9+.-]*:/i.test(url)
export default function HomeLink({ url, className, style, children }: HomeLinkProps) {
if (url.startsWith('/') && !url.startsWith('//')) {
return (
<Link to={url} className={className} style={style}>
{children}
</Link>
)
}
// mailto: and tel: open an app, not a tab.
const newTab = isExternal(url) && !/^(mailto|tel):/i.test(url)
return (
<a
href={url}
className={className}
style={style}
{...(newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
>
{children}
</a>
)
}