Skip to main content

Acceptance Spec — Pro Website: Hero Type (video / slideshow / video-then-slideshow)

  • Status: APPROVED (founder, 2026-06-02) — build in progress
  • Owner: ChurchWiseAI (CWA)
  • Surface: churchwiseai.com/admin/[token] (Website → Hero → "Your own photos") + public /s/[slug] (UnifiedTemplate / VideoHero)
  • Feature Registry id: cwa-pro-website-hero-media-mode
  • Plan gating: Pro Website — same as the rest of the Website editor
  • Critical path: touches src/app/s/[slug]/page.tsx + src/app/api/premium/update/route.ts (both critical_path: true) → deployed Playwright verification mandatory.

Why

A church (Kevin Soodsma, Beckwith Hills CRC) had a custom-rendered hero video AND uploaded 8 hero photos. The old behavior silently replaced the video with the photo slideshow the moment any custom photo existed (UnifiedTemplate.tsx: videoSrc={useCustomSlideshow ? undefined : …}), so his MP4 stopped showing and only one photo appeared. Founder intent: play the hero video first, then the photos — and give the church an explicit choice.

Audit (2026-06-02): exactly 1 church had both a custom video and custom photos; 0 had photos-only. So the new auto-default changes behavior for that one church only (the fix), and is a no-op for the other ~3,164 premium rows.

Data model (additive — no data migration)

premium_churches.hero_media_mode (+ draft_hero_media_mode), nullable text, CHECK ∈ {video, slideshow, video_then_slideshow} or NULL. Migration: churchwiseai-web/migrations/20260602_add_hero_media_mode.sql. Draft twin registered in pro-website-draft.ts (DRAFT_TO_CANONICAL + select list).

hero_media_mode:
'video' — loop the hero video only (no slideshow)
'slideshow' — customer photos only (Ken Burns, no video/sound)
'video_then_slideshow' — play the hero video once, then cycle the photos
NULL — auto (see resolveHeroMediaMode)

Resolution (single source of truth)

resolveHeroMediaMode({ stored, hasCustomHeroVideo, hasCustomPhotos }) in src/lib/premium-shared.ts (pure, shared by renderer + editor):

  • a stored value always wins;
  • NULL → video_then_slideshow when both a custom hero video AND photos exist;
  • NULL → slideshow when only photos exist;
  • NULL → video otherwise.

Public render (UnifiedTemplateVideoHero)

  • video_then_slideshow: videoSrc set, slideshowStandalone=false, slideshowImages=customPhotos. VideoHero plays the video once (loop={!hasSlideshow}), then onEnded hands off to the Ken Burns slideshow.
  • slideshow: videoSrc=undefined, slideshowStandalone=true, first photo is the static base before the slideshow starts; while the slideshow cycles, the base layer tracks the INCOMING slide so the crossfade never ghosts a third image (stutter fix, 2026-06-10). Photos cycle (Ken Burns + crossfade; the zoom runs continuously per layer — staggered infinite-alternate — so the outgoing slide no longer snaps back to scale(1) mid-fade).
  • video: videoSrc set, slideshowImages=[] → the video loops, no slideshow.
  • No custom photos (unchanged): legacy behavior preserved — the video plays, then any curated library-poster slideshow (hero_slideshow_keys) cycles after.
  • Reduced motion: per existing VideoHero behavior, autoplay + slideshow are suppressed and a single static hero image shows (accessibility — by design).

Admin editor (Website → Hero → "Your own photos")

  • When ≥1 photo is uploaded, a "How should the hero play?" radio group shows the 3 modes with helper text. The active selection reflects the resolved effective mode (so it matches reality even before the church picks).
  • While the church hasn't explicitly chosen, the effective mode tracks the current photo count (first upload defaults to "video, then photos" when a video exists, else "photos only"). An explicit pick is submitted as a concrete value.
  • Draft-aware (DraftAwareSaveFormdraft_hero_media_mode); Publish copies it to canonical via DRAFT_TO_CANONICAL. Test ids: ps-admin-hero-mode-{mode}.
  • Copy corrected: the old "they replace the hero video" line is removed.

Server validation

api/premium/update, website section: hero_media_mode coerced to the allowlist (else NULL); routed to draft_hero_media_mode when target=website_draft via the existing CANONICAL_TO_DRAFT remap.

Graceful degradation / regression guards

  • ~3,164 churches with no custom photos: identical hero behavior to before.
  • A church that never opens the control keeps hero_media_mode = NULL and the renderer resolves it live — Beckwith self-heals to video_then_slideshow with zero data writes.
  • Library-poster slideshow (hero_slideshow_keys) behavior is untouched.

Verification (evidence-or-nothing)

  • pnpm build + scoped pnpm lint green.
  • Playwright on the deployed preview against the Beckwith demo (or a demo church with a hero video + ≥2 photos): set each of the 3 modes, Publish, load /s/[slug], and confirm — (a) video_then_slideshow shows the video then cycles photos; (b) slideshow cycles photos with no video; (c) video loops the video with no slideshow. Confirm a church with no custom photos is unchanged.