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>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
/* ═══════════════════════════════════════════════════════════════
|
|
EMBEDS
|
|
|
|
Turns the link an admin pastes into the URL an <iframe> can
|
|
load. People paste the page they're looking at — a YouTube watch
|
|
or live link, a Facebook video, a Vimeo page — not the embed
|
|
form, so those are recognised and rewritten. Anything else that
|
|
is https is assumed to already be an embed URL and passed
|
|
through; anything that isn't https is refused, so the hero never
|
|
frames a plain-http or javascript: URL.
|
|
|
|
Streams start muted: browsers only autoplay muted video, and a
|
|
hero that starts shouting is worse than one that asks.
|
|
═══════════════════════════════════════════════════════════════ */
|
|
|
|
function youtubeId(url: URL): string | null {
|
|
const host = url.hostname.replace(/^www\.|^m\./, '')
|
|
if (host === 'youtu.be') return url.pathname.slice(1) || null
|
|
if (host !== 'youtube.com' && host !== 'youtube-nocookie.com') return null
|
|
|
|
const v = url.searchParams.get('v')
|
|
if (v) return v
|
|
|
|
// /live/ID, /embed/ID, /shorts/ID
|
|
const match = url.pathname.match(/^\/(?:live|embed|shorts)\/([\w-]+)/)
|
|
return match?.[1] ?? null
|
|
}
|
|
|
|
/** An iframe src for a pasted stream link, or null if it can't be framed. */
|
|
export function livestreamEmbedUrl(raw?: string | null): string | null {
|
|
if (!raw) return null
|
|
|
|
let url: URL
|
|
try {
|
|
url = new URL(raw.trim())
|
|
} catch {
|
|
return null
|
|
}
|
|
if (url.protocol !== 'https:') return null
|
|
|
|
const yt = youtubeId(url)
|
|
if (yt) {
|
|
return `https://www.youtube-nocookie.com/embed/${encodeURIComponent(yt)}?autoplay=1&mute=1&playsinline=1`
|
|
}
|
|
|
|
const host = url.hostname.replace(/^www\./, '')
|
|
|
|
if (host === 'vimeo.com') {
|
|
const id = url.pathname.match(/^\/(?:event\/)?(\d+)/)?.[1]
|
|
if (id) {
|
|
return url.pathname.startsWith('/event/')
|
|
? `https://vimeo.com/event/${id}/embed?autoplay=1&muted=1`
|
|
: `https://player.vimeo.com/video/${id}?autoplay=1&muted=1`
|
|
}
|
|
}
|
|
|
|
if ((host === 'facebook.com' || host === 'fb.watch') && !url.pathname.startsWith('/plugins/')) {
|
|
return `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(url.href)}&autoplay=1&mute=1`
|
|
}
|
|
|
|
return url.href
|
|
}
|