1896b707f8
Twelve narrated walkthroughs, one per Suite tool, in Marty's cloned voice. No screen recording involved: Playwright drives and records the real UI, ElevenLabs narrates, ffmpeg muxes. Pipeline lives outside this repo at ../video-pipeline. Placement (Marty's call): TOP of each tool page but COLLAPSED — a one-line "Watch the walkthrough" bar that auto-opens on a member's first visit to that specific tool and stays shut after. A video in a separate library never gets watched; an embedded player pushes the tool below the fold on mobile for every returning member. First-visit autoplay is muted, so it is never a surprise noise, and every localStorage access is guarded because it throws outright in private windows. Served from the data volume via /tv/<slug>.mp4, NOT from public/. That directory is already 199MB and another 33MB of MP4 would ride along on every deploy forever; this way a video can also be re-cut and dropped in without a rebuild. The route implements HTTP range requests — without them the scrub bar renders but cannot seek, which looks broken in a way people rarely report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
4.6 KiB
JavaScript
113 lines
4.6 KiB
JavaScript
// Collapsible walkthrough player for Suite tool pages.
|
|
//
|
|
// Placement reasoning (Marty's call): the video belongs at the TOP of the tool,
|
|
// because a video in a separate library never gets watched — but it must not be
|
|
// an embedded player, because after the first week most visits are returning
|
|
// members who came to USE the tool, and a player pushes the tool below the fold
|
|
// on mobile every single time.
|
|
//
|
|
// So: a one-line bar that expands inline, auto-opened on the member's FIRST
|
|
// visit to that specific tool and collapsed on every visit after. The state is
|
|
// per-tool, per-browser. localStorage can throw outright in some contexts
|
|
// (private windows, blocked site data), so every access is guarded and the bar
|
|
// falls back to "collapsed but present" rather than breaking the page.
|
|
(function () {
|
|
'use strict';
|
|
|
|
var el = document.currentScript;
|
|
var SRC = el && el.getAttribute('data-video');
|
|
var TITLE = (el && el.getAttribute('data-title')) || 'Watch the walkthrough';
|
|
var KEY = 'rmc.tv.' + ((el && el.getAttribute('data-key')) || (location.pathname.replace(/\W+/g, '') || 'tool'));
|
|
var SECS = (el && el.getAttribute('data-secs')) || '';
|
|
if (!SRC) return;
|
|
|
|
function seen() { try { return localStorage.getItem(KEY) === '1'; } catch (e) { return true; } }
|
|
function markSeen() { try { localStorage.setItem(KEY, '1'); } catch (e) {} }
|
|
|
|
function build() {
|
|
var host = document.querySelector('main') || document.body;
|
|
var wrap = document.createElement('div');
|
|
wrap.className = 'tv-wrap';
|
|
|
|
var bar = document.createElement('button');
|
|
bar.type = 'button';
|
|
bar.className = 'tv-bar';
|
|
bar.setAttribute('aria-expanded', 'false');
|
|
bar.innerHTML = '<span class="tv-play">▶</span><span class="tv-t">' + TITLE +
|
|
(SECS ? ' <span class="tv-secs">' + SECS + '</span>' : '') + '</span><span class="tv-chev">⌄</span>';
|
|
|
|
var body = document.createElement('div');
|
|
body.className = 'tv-body';
|
|
body.hidden = true;
|
|
|
|
var vid = document.createElement('video');
|
|
vid.controls = true;
|
|
vid.preload = 'none';
|
|
vid.playsInline = true;
|
|
vid.src = SRC;
|
|
body.appendChild(vid);
|
|
|
|
function open(autoplay) {
|
|
body.hidden = false;
|
|
wrap.classList.add('on');
|
|
bar.setAttribute('aria-expanded', 'true');
|
|
if (autoplay) {
|
|
// Muted so a first-visit autoplay is never a surprise noise; the member
|
|
// unmutes if they want it. Browsers block unmuted autoplay anyway.
|
|
vid.muted = true;
|
|
vid.play().catch(function () {});
|
|
}
|
|
}
|
|
function close() {
|
|
body.hidden = true;
|
|
wrap.classList.remove('on');
|
|
bar.setAttribute('aria-expanded', 'false');
|
|
vid.pause();
|
|
}
|
|
|
|
bar.addEventListener('click', function () {
|
|
if (body.hidden) { open(false); markSeen(); }
|
|
else close();
|
|
});
|
|
|
|
wrap.appendChild(bar);
|
|
wrap.appendChild(body);
|
|
|
|
// Sit directly under the page heading if there is one, otherwise at the top.
|
|
var h1 = host.querySelector('h1');
|
|
var lede = h1 && h1.nextElementSibling && /lede|sub/.test(h1.nextElementSibling.className || '')
|
|
? h1.nextElementSibling : h1;
|
|
if (lede && lede.parentNode) lede.parentNode.insertBefore(wrap, lede.nextSibling);
|
|
else host.insertBefore(wrap, host.firstChild);
|
|
|
|
if (!seen()) { open(true); markSeen(); }
|
|
}
|
|
|
|
function style() {
|
|
if (document.getElementById('tv-style')) return;
|
|
var s = document.createElement('style');
|
|
s.id = 'tv-style';
|
|
s.textContent = [
|
|
'.tv-wrap{margin:0 0 18px}',
|
|
'.tv-bar{display:flex;align-items:center;gap:10px;width:100%;text-align:left;cursor:pointer;',
|
|
' background:rgba(212,175,55,.08);border:1px solid var(--gold,#d4af37);border-radius:12px;',
|
|
' color:var(--text,#e8eef4);padding:11px 15px;font:inherit;font-size:14.5px;font-weight:700}',
|
|
'.tv-bar:hover{background:rgba(212,175,55,.14)}',
|
|
'.tv-bar:focus-visible{outline:2px solid var(--teal,#4ed6cb);outline-offset:2px}',
|
|
'.tv-play{color:var(--gold,#d4af37);font-size:12px}',
|
|
'.tv-t{flex:1}',
|
|
'.tv-secs{color:var(--muted,#8fa6bb);font-weight:600;font-size:12.5px}',
|
|
'.tv-chev{color:var(--muted,#8fa6bb);transition:transform .18s}',
|
|
'.tv-wrap.on .tv-chev{transform:rotate(180deg)}',
|
|
'.tv-body{margin-top:10px;border:1px solid var(--line,#24425d);border-radius:12px;overflow:hidden;line-height:0}',
|
|
'.tv-body video{width:100%;height:auto;display:block;background:#000}',
|
|
'@media (prefers-reduced-motion:reduce){.tv-chev{transition:none}}'
|
|
].join('');
|
|
document.head.appendChild(s);
|
|
}
|
|
|
|
function go() { style(); build(); }
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go);
|
|
else go();
|
|
})();
|