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:
parent
b4b013209b
commit
6a69084de2
25 changed files with 2299 additions and 494 deletions
189
server/src/migrations/017_front_page.sql
Normal file
189
server/src/migrations/017_front_page.sql
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- FRONT PAGE
|
||||
--
|
||||
-- The home page's editable half. One row in front_page — the CHECK
|
||||
-- on id makes a second one impossible — and ordered collections
|
||||
-- hanging off it, each replaced wholesale on save the way every
|
||||
-- other child collection is. Nothing outside this file has a
|
||||
-- foreign key into any of them, which is what makes that safe.
|
||||
--
|
||||
-- front_page the hero: its words, its buttons, and
|
||||
-- which mode it's in
|
||||
-- front_page_slides photos the hero cycles through in
|
||||
-- 'photos' mode
|
||||
-- front_page_sections which bands the page draws, in what
|
||||
-- order, under what heading
|
||||
-- front_page_stats the numbers band; each one typed in or
|
||||
-- counted from the database
|
||||
-- front_page_paths the connect section's "I want to…"
|
||||
-- choices, each with its actions
|
||||
-- front_page_path_actions
|
||||
--
|
||||
-- What stays in code: how each section looks, and the list of
|
||||
-- section keys. A section is a component, so the CHECK on
|
||||
-- front_page_sections.section is the list of components that
|
||||
-- exist; a row can reorder, retitle or hide one, never invent one.
|
||||
--
|
||||
-- hero_mode is switched by hand. 'livestream' shows the embed with
|
||||
-- a LIVE badge until someone switches it back — no schedule, so no
|
||||
-- guessing whose timezone a start time was typed in.
|
||||
--
|
||||
-- countdown_event_id pins the countdown to one event. Null counts
|
||||
-- down to the next upcoming published event, which is what it
|
||||
-- should do almost always.
|
||||
--
|
||||
-- Stats: source says where the number comes from. 'manual' prints
|
||||
-- value as typed. 'years_since' reads value as a year and counts up
|
||||
-- from it. Everything else is a COUNT the API runs, so the band
|
||||
-- never goes stale. Adding a source is this CHECK, the enum in both
|
||||
-- descriptor halves, and the query in routes/home.js.
|
||||
--
|
||||
-- The seed is the page as it ships: every section, the stats that
|
||||
-- need no typing, and the Church Center forms that were hardcoded
|
||||
-- on the old home page, sorted into paths.
|
||||
--
|
||||
-- The updated_at trigger is in 018, on its own, so no statement
|
||||
-- here sits after a BEGIN...END body.
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE front_page (
|
||||
id TEXT PRIMARY KEY CHECK (id = 'home'),
|
||||
|
||||
hero_mode TEXT NOT NULL DEFAULT 'brand'
|
||||
CHECK (hero_mode IN ('brand', 'photos', 'livestream')),
|
||||
eyebrow TEXT,
|
||||
headline TEXT NOT NULL DEFAULT 'Next Generation of Unity',
|
||||
subhead TEXT,
|
||||
primary_label TEXT,
|
||||
primary_url TEXT,
|
||||
secondary_label TEXT,
|
||||
secondary_url TEXT,
|
||||
|
||||
slide_seconds INTEGER NOT NULL DEFAULT 7
|
||||
CHECK (slide_seconds BETWEEN 3 AND 60),
|
||||
|
||||
livestream_url TEXT,
|
||||
livestream_title TEXT,
|
||||
|
||||
countdown_event_id TEXT REFERENCES events (id) ON DELETE SET NULL,
|
||||
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE front_page_slides (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
page_id TEXT NOT NULL REFERENCES front_page (id) ON DELETE CASCADE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
media TEXT NOT NULL, -- filename in public/front-page/, or a URL
|
||||
alt TEXT,
|
||||
caption TEXT,
|
||||
link_url TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE front_page_sections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
page_id TEXT NOT NULL REFERENCES front_page (id) ON DELETE CASCADE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
section TEXT NOT NULL
|
||||
CHECK (section IN ('countdown', 'retreats', 'stats', 'timeline', 'connect')),
|
||||
title TEXT, -- null → the section's own heading
|
||||
blurb TEXT,
|
||||
-- Hidden rather than visible, so a freshly added row with nothing
|
||||
-- ticked is still a blank row the engine can drop.
|
||||
is_hidden INTEGER NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1)),
|
||||
UNIQUE (page_id, section)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE front_page_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
page_id TEXT NOT NULL REFERENCES front_page (id) ON DELETE CASCADE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
label TEXT NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT 'manual'
|
||||
CHECK (source IN ('manual', 'years_since', 'regions', 'chapters',
|
||||
'partners', 'events_held', 'retreats_held',
|
||||
'people', 'awards_given')),
|
||||
value TEXT,
|
||||
suffix TEXT, -- '+', 'k', ' states'
|
||||
note TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE front_page_paths (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
page_id TEXT NOT NULL REFERENCES front_page (id) ON DELETE CASCADE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
label TEXT NOT NULL, -- 'Attend'
|
||||
icon TEXT, -- one emoji
|
||||
blurb TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE front_page_path_actions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
path_id INTEGER NOT NULL REFERENCES front_page_paths (id) ON DELETE CASCADE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
label TEXT NOT NULL,
|
||||
description TEXT,
|
||||
url TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX front_page_path_actions_path_idx ON front_page_path_actions (path_id, sort_order);
|
||||
|
||||
-- ── Seed ─────────────────────────────────────────────────────────
|
||||
|
||||
INSERT INTO front_page
|
||||
(id, eyebrow, headline, subhead,
|
||||
primary_label, primary_url, secondary_label, secondary_url)
|
||||
VALUES
|
||||
('home',
|
||||
'Young adults of the Unity movement',
|
||||
'Next Generation of Unity',
|
||||
'A community for 18–40 year olds, rooted in spiritual growth, leadership and sacred service.',
|
||||
'Find a retreat', '/retreats',
|
||||
'Find your way in', '#connect');
|
||||
|
||||
INSERT INTO front_page_sections (page_id, sort_order, section, title, blurb) VALUES
|
||||
('home', 0, 'countdown', NULL, NULL),
|
||||
('home', 1, 'retreats', 'National Retreats', 'Our flagship gatherings, open to young adults across the country.'),
|
||||
('home', 2, 'stats', 'NGU by the numbers', NULL),
|
||||
('home', 3, 'timeline', 'Moments that shaped us', 'Highlights from our history.'),
|
||||
('home', 4, 'connect', 'Find your way in', 'Tell us what you''re looking for.');
|
||||
|
||||
INSERT INTO front_page_stats (page_id, sort_order, label, source) VALUES
|
||||
('home', 0, 'Regions', 'regions'),
|
||||
('home', 1, 'Chapters', 'chapters'),
|
||||
('home', 2, 'Retreats held', 'retreats_held'),
|
||||
('home', 3, 'Awards given', 'awards_given');
|
||||
|
||||
INSERT INTO front_page_paths (page_id, sort_order, label, icon, blurb) VALUES
|
||||
('home', 0, 'Attend', '🧭', 'Come to a gathering near you or across the country.'),
|
||||
('home', 1, 'Serve', '🤲', 'Help create transformative experiences for young adults.'),
|
||||
('home', 2, 'Belong', '🌱', 'Make NGU your community.'),
|
||||
('home', 3, 'Partner', '🤝', 'Bring your ministry or organization alongside us.');
|
||||
|
||||
INSERT INTO front_page_path_actions (path_id, sort_order, label, description, url)
|
||||
SELECT p.id, a.sort_order, a.label, a.description, a.url
|
||||
FROM front_page_paths p
|
||||
JOIN (
|
||||
SELECT 'Attend' AS path, 0 AS sort_order, 'See upcoming retreats' AS label,
|
||||
'National, regional and partner gatherings.' AS description,
|
||||
'/retreats' AS url
|
||||
UNION ALL SELECT 'Attend', 1, 'NGU calendar',
|
||||
'Everything on the schedule, in one place.',
|
||||
'https://ngu.churchcenter.com/calendar?view=gallery'
|
||||
UNION ALL SELECT 'Serve', 0, 'Volunteer',
|
||||
'Lend a hand at a retreat or event.',
|
||||
'https://ngu.churchcenter.com/people/forms/1176908'
|
||||
UNION ALL SELECT 'Serve', 1, 'Speaker & Musician Directory',
|
||||
'Join our network of speakers, musicians and facilitators.',
|
||||
'https://ngu.churchcenter.com/people/forms/1173181'
|
||||
UNION ALL SELECT 'Belong', 0, 'Become a member',
|
||||
'Join the NGU community officially.',
|
||||
'https://ngu.churchcenter.com/people/forms/1135816'
|
||||
UNION ALL SELECT 'Belong', 1, 'Find your region',
|
||||
'Chapters and regions across the country.',
|
||||
'/community'
|
||||
UNION ALL SELECT 'Partner', 0, 'Affiliation form',
|
||||
'Affiliate your ministry or spiritual organization with NGU.',
|
||||
'https://ngu.churchcenter.com/people/forms/1135750'
|
||||
) a ON a.path = p.label
|
||||
WHERE p.page_id = 'home';
|
||||
Loading…
Add table
Add a link
Reference in a new issue