feat(landing): honest high-end redesign — kill fake social proof, brand gradient identity, final CTA, mobile fixes
- removed fabricated fork counts/'verified' badges, 'our customers ship today' copy, pseudo client logo marks and stale v0.1 badge (zero-user product must not fake traction) - new proof-by-specificity band: verifiable links to /docs/oauth, /status, /security instead of testimonial cosplay - identity: indigo-to-cyan brand gradient, indigo-tinted elevated surfaces, mono section kickers, terminal-chrome hero rotator, gradient h-11 CTA - how-it-works as connected pipeline; new final CTA band (page no longer ends on FAQ); footer upgraded to 4-column product/resources/legal - lib/pricing.ts single tier source for pricing page + landing teaser; annual-billing FAQ claim softened to truth (no annual checkout exists) - hero video preload=metadata + IntersectionObserver play (2.6MB off the critical path); 44px touch targets; mobile menu: Guides link, CTA, scroll-lock, escape/outside-tap close Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
This commit is contained in:
@@ -47,6 +47,9 @@ function formatTime(seconds: number): string {
|
||||
export function HeroVideo() {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
// Set when the user explicitly pauses — the IntersectionObserver must not
|
||||
// restart a video the user chose to stop.
|
||||
const userPausedRef = useRef(false);
|
||||
const [muted, setMuted] = useState(true);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [playFailed, setPlayFailed] = useState(false);
|
||||
@@ -69,10 +72,26 @@ export function HeroVideo() {
|
||||
v.addEventListener('timeupdate', onTime);
|
||||
v.addEventListener('loadedmetadata', onMeta);
|
||||
v.addEventListener('durationchange', onMeta);
|
||||
// Best-effort autoplay attempt — silently fail on browsers that
|
||||
// block it; the overlay is the user's escape hatch.
|
||||
v.play().catch(() => undefined);
|
||||
// Play only while scrolled into view: with preload="metadata" the 2.6 MB
|
||||
// mp4 stays off the critical path until the section is actually visible,
|
||||
// and playback pauses again off-screen (saves battery + decode). The
|
||||
// .play() attempt is still best-effort — browsers that block autoplay
|
||||
// leave the overlay as the user's escape hatch. `userPaused` tracks an
|
||||
// explicit pause so the observer doesn't fight the user's choice.
|
||||
const io = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry) return;
|
||||
if (entry.isIntersecting) {
|
||||
if (!userPausedRef.current && v.paused) v.play().catch(() => undefined);
|
||||
} else if (!v.paused) {
|
||||
v.pause();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.35 },
|
||||
);
|
||||
io.observe(v);
|
||||
return () => {
|
||||
io.disconnect();
|
||||
v.removeEventListener('play', onPlay);
|
||||
v.removeEventListener('pause', onPause);
|
||||
v.removeEventListener('timeupdate', onTime);
|
||||
@@ -82,9 +101,12 @@ export function HeroVideo() {
|
||||
}, []);
|
||||
|
||||
// Clear any pending hide timer on unmount.
|
||||
useEffect(() => () => {
|
||||
if (hideTimer.current) clearTimeout(hideTimer.current);
|
||||
}, []);
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (hideTimer.current) clearTimeout(hideTimer.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (hideTimer.current) clearTimeout(hideTimer.current);
|
||||
@@ -106,6 +128,7 @@ export function HeroVideo() {
|
||||
if (!v) return;
|
||||
if (v.paused) {
|
||||
setPlayFailed(false);
|
||||
userPausedRef.current = false;
|
||||
try {
|
||||
// .load() resets the media element's resource selection — required
|
||||
// when an earlier autoplay attempt was blocked before the source
|
||||
@@ -116,6 +139,7 @@ export function HeroVideo() {
|
||||
setPlayFailed(true);
|
||||
}
|
||||
} else {
|
||||
userPausedRef.current = true;
|
||||
v.pause();
|
||||
}
|
||||
}, []);
|
||||
@@ -165,11 +189,10 @@ export function HeroVideo() {
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="auto"
|
||||
preload="metadata"
|
||||
poster="/videos/hero-poster.jpg"
|
||||
onClick={onVideoClick}
|
||||
className="size-full cursor-pointer object-cover"
|
||||
|
||||
Reference in New Issue
Block a user