buildmymcpserver/apps/web/components/hero-video.tsx
Marco Sadjadi 3a05766f88
All checks were successful
Deploy to Production / deploy (push) Successful in 1m28s
fix(oauth): allow generic RFC 7591 DCR + expand install snippets
- /oauth/register: drop resource_required check, accept generic
  registrations (Claude Desktop omits resource in DCR body per spec).
  serverId stored as NULL; /authorize still enforces org-ownership
  + access-token aud claim still pinned to resource. Fixes Claude
  Desktop DCR failure (ofid_d7e39530c109fa7f).
- /oauth/authorize: skip strict server.id check when client.serverId
  is NULL (generic client); org check remains the security boundary.
- schema: oauth_clients.server_id no longer NOT NULL.
- migration 0002: ALTER COLUMN server_id DROP NOT NULL (already
  applied on prod).
- install-snippets: add Claude Code (CLI), VS Code, Codex, raw URL
  tabs. Claude Desktop now shows form-field values (Name / Remote MCP
  Server URL / OAuth Client ID / Secret) matching the new Custom
  Connector UI instead of the obsolete JSON config.
- types: InstallTarget enum extended.
- hero-video: clicking the audio toggle restarts the video from
  frame 0 so unmute aligns with the spoken opening.
- marketing: drop em-dashes from rendered copy.
2026-05-28 17:20:01 +02:00

187 lines
7.0 KiB
TypeScript

'use client';
import { ExternalLink, Play, Volume2, VolumeX } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
/**
* Hero video player tuned for the stubborn-autoplay cases we hit in
* the field (Chrome with prefers-reduced-motion, data saver, etc.).
*
* Three failure modes we defend against:
*
* 1. Browser allows muted autoplay — the happy path. Element loads,
* starts playing, the `play` event flips `playing` to true and
* hides the overlay.
*
* 2. Browser blocks autoplay but allows playback on user gesture —
* the play overlay sits over the poster, the user clicks, we
* call `.load()` first to reset the resource-selection state
* (some Chrome builds park the element at networkState=2/
* readyState=0 forever if the original autoplay was blocked
* before the source ever fetched) and then `.play()` with the
* user gesture in scope. Plays.
*
* 3. Browser refuses to play even after the gesture — extension-
* sandboxed contexts, hardware-decoder failures, etc. We catch
* the promise rejection, surface a small "open video in a new
* tab" link so the visitor isn't completely stuck.
*
* Source order: MP4 only. We previously offered WebM (VP9) first as a
* size win, but Chrome will pick WebM if listed first, and if that
* decode fails it does NOT fall back to MP4 — it just sits in an
* unloaded state. Single MP4 source (Main profile / yuv420p / TV-
* range / faststart) plays everywhere and the file is 2.6 MB which is
* close enough to the WebM that the extra HTTP source-juggling isn't
* worth the failure surface.
*/
export function HeroVideo() {
const videoRef = useRef<HTMLVideoElement>(null);
const [muted, setMuted] = useState(true);
const [playing, setPlaying] = useState(false);
const [playFailed, setPlayFailed] = useState(false);
useEffect(() => {
const v = videoRef.current;
if (!v) return;
const onPlay = () => {
setPlaying(true);
setPlayFailed(false);
};
const onPause = () => setPlaying(false);
v.addEventListener('play', onPlay);
v.addEventListener('pause', onPause);
// Best-effort autoplay attempt — silently fail on browsers that
// block it; the overlay is the user's escape hatch.
v.play().catch(() => undefined);
return () => {
v.removeEventListener('play', onPlay);
v.removeEventListener('pause', onPause);
};
}, []);
const togglePlay = useCallback(async () => {
const v = videoRef.current;
if (!v) return;
if (v.paused) {
setPlayFailed(false);
try {
// .load() resets the media element's resource selection — required
// when an earlier autoplay attempt was blocked before the source
// ever fetched. Without it, .play() on a stuck element no-ops.
v.load();
await v.play();
} catch {
setPlayFailed(true);
}
} else {
v.pause();
}
}, []);
const toggleMute = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const v = videoRef.current;
if (!v) return;
const next = !v.muted;
v.muted = next;
setMuted(next);
// Restart from frame 0 whenever the audio state toggles. Visitors who
// unmute want to hear the opening, not whatever moment the video was
// already at; visitors who re-mute also expect a clean restart so the
// animation lines up with the silent loop again.
v.currentTime = 0;
if (v.paused) {
v.play().catch(() => undefined);
}
}, []);
return (
<>
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
preload="auto"
poster="/videos/hero-poster.jpg"
onClick={togglePlay}
className="size-full cursor-pointer object-cover"
aria-label="Animation: a prompt becomes a live MCP server, with secrets staying isolated from the AI pipeline"
>
{/* MP4 only — see file-header note on dropping the WebM source. */}
<source src="/videos/hero.mp4" type="video/mp4" />
</video>
{/* PLAY overlay — visible while paused. Full-frame so clicking
anywhere starts playback (YouTube / Vimeo pattern). */}
{!playing && (
<button
type="button"
onClick={togglePlay}
aria-label="Play video"
className="group absolute inset-0 z-10 flex items-center justify-center"
style={{
backgroundColor:
'color-mix(in oklab, var(--color-bg) 30%, transparent)',
}}
>
<div
className="flex size-20 items-center justify-center rounded-full border backdrop-blur transition-transform duration-200 ease-out group-hover:scale-110"
style={{
backgroundColor:
'color-mix(in oklab, var(--color-bg-elevated) 80%, transparent)',
borderColor: 'var(--color-accent)',
boxShadow:
'0 0 32px rgba(99, 102, 241, 0.45), 0 12px 40px rgba(0,0,0,0.55)',
color: 'var(--color-accent)',
}}
>
{/* translate-x-0.5 optically centres the triangle */}
<Play size={32} fill="currentColor" className="translate-x-0.5" />
</div>
</button>
)}
{/* Fallback escape hatch — surfaces only if .play() rejects even
after a user gesture (extension sandbox, hardware-decoder
failures, etc.). One-line link to the raw MP4 so the visitor
isn't trapped staring at a poster forever. */}
{playFailed && !playing && (
<a
href="/videos/hero.mp4"
target="_blank"
rel="noopener noreferrer"
className="absolute inset-x-0 bottom-20 z-20 mx-auto flex w-fit items-center gap-2 rounded-md border px-3 py-2 text-[12px] backdrop-blur hover:text-[--color-fg]"
style={{
backgroundColor:
'color-mix(in oklab, var(--color-bg-elevated) 85%, transparent)',
borderColor: 'var(--color-border-strong)',
color: 'var(--color-fg-muted)',
}}
>
your browser blocked playback. open the video directly
<ExternalLink size={12} />
</a>
)}
{/* Mute toggle — always visible, top of z-stack. */}
<button
type="button"
onClick={toggleMute}
aria-label={muted ? 'Unmute video' : 'Mute video'}
aria-pressed={!muted}
className="absolute bottom-4 right-4 z-30 inline-flex size-10 items-center justify-center rounded-full border backdrop-blur transition-colors duration-150 hover:text-[--color-fg] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]"
style={{
backgroundColor:
'color-mix(in oklab, var(--color-bg-elevated) 75%, transparent)',
borderColor: 'var(--color-border)',
color: muted ? 'var(--color-fg-muted)' : 'var(--color-accent)',
}}
>
{muted ? <VolumeX size={18} /> : <Volume2 size={18} />}
</button>
</>
);
}