Event Video
Event-promo link-in-bio with a 16:9 video at the top (poster + click-to-play YouTube/Vimeo embed), big Fraunces title, date + location strip, live countdown timer (days / hours / mins / secs),
Preview
Source
tsx
"use client";
import { useEffect, useState } from "react";
import {
Instagram,
Twitter,
Youtube,
Mail,
Globe,
ShoppingBag,
PlayCircle,
Linkedin,
Music2,
ArrowUpRight,
Play,
Calendar,
MapPin,
Sparkles,
CheckCircle2,
type LucideIcon,
} from "lucide-react";
import type { LinkIcon, LinksInBioData } from "../types";
import { defaultData } from "../defaultData";
const ICONS: Record<LinkIcon, LucideIcon> = {
instagram: Instagram,
twitter: Twitter,
tiktok: Music2,
youtube: Youtube,
spotify: Music2,
apple: Music2,
linkedin: Linkedin,
email: Mail,
globe: Globe,
shop: ShoppingBag,
play: PlayCircle,
};
const PAPER = "#0e0d12";
const CARD = "#16151c";
const INK = "#f6f4ed";
const ACCENT = "#ff6f4d";
const RULE = "rgba(255,255,255,0.10)";
function useCountdown(target: string) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (typeof window === "undefined") return;
const t = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(t);
}, []);
const ms = Math.max(0, new Date(target).getTime() - now);
const days = Math.floor(ms / 86400000);
const hours = Math.floor((ms % 86400000) / 3600000);
const mins = Math.floor((ms % 3600000) / 60000);
const secs = Math.floor((ms % 60000) / 1000);
return { days, hours, mins, secs, done: ms <= 0 };
}
function CountdownPill({
value,
label,
}: {
value: number;
label: string;
}) {
return (
<div
className="flex flex-col items-center justify-center rounded-xl border px-3 py-3 text-center"
style={{ background: CARD, borderColor: RULE, color: INK, minWidth: 64 }}
>
<span className="text-2xl font-bold tabular-nums">
{String(value).padStart(2, "0")}
</span>
<span className="mt-0.5 text-[10px] uppercase tracking-[0.22em] text-white/55">
{label}
</span>
</div>
);
}
export interface LinksInBioEventVideoProps {
data?: LinksInBioData;
}
export default function LinksInBioEventVideo({
data = defaultData,
}: LinksInBioEventVideoProps) {
const event = data.event;
const [playing, setPlaying] = useState(false);
const [email, setEmail] = useState("");
const [signedUp, setSignedUp] = useState(false);
const { days, hours, mins, secs, done } = useCountdown(
event?.startsAt ?? new Date().toISOString()
);
const formattedDate = event
? new Date(event.startsAt).toLocaleDateString(undefined, {
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
})
: "";
const formattedTime = event
? new Date(event.startsAt).toLocaleTimeString(undefined, {
hour: "numeric",
minute: "2-digit",
})
: "";
return (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Fraunces:ital,wght@0,400..900;1,400..900&display=swap"
/>
<section
className="min-h-screen w-full pb-12"
style={{ background: PAPER, color: INK, fontFamily: "'Inter', sans-serif" }}
>
{/* Top label */}
<div className="mx-auto w-full max-w-[640px] px-5 pt-6">
<div
className="mx-auto flex w-fit items-center gap-2 rounded-full border px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.25em]"
style={{ borderColor: ACCENT, color: ACCENT }}
>
<span
aria-hidden
className="inline-block h-1.5 w-1.5 animate-pulse rounded-full"
style={{ background: ACCENT }}
/>
Live event
</div>
</div>
{/* Video */}
{event && (
<div className="mx-auto mt-5 w-full max-w-[640px] px-3">
<div
className="relative aspect-video w-full overflow-hidden rounded-2xl"
style={{ background: "#000", boxShadow: "0 24px 60px rgba(0,0,0,0.5)" }}
>
{playing && event.videoEmbed ? (
<iframe
title={event.title}
src={`${event.videoEmbed}${event.videoEmbed.includes("?") ? "&" : "?"}autoplay=1`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="absolute inset-0 h-full w-full"
/>
) : (
<button
type="button"
onClick={() => setPlaying(true)}
className="group absolute inset-0 flex items-center justify-center"
aria-label="Play event preview"
>
{event.videoThumb && (
<img
src={event.videoThumb}
alt=""
width={1280}
height={720}
loading="eager"
className="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]"
/>
)}
<div
aria-hidden
className="absolute inset-0"
style={{
background:
"linear-gradient(180deg, rgba(0,0,0,0.0) 30%, rgba(0,0,0,0.65) 100%)",
}}
/>
<span
className="relative flex h-16 w-16 items-center justify-center rounded-full transition-transform group-hover:scale-105"
style={{
background: ACCENT,
boxShadow: "0 12px 30px rgba(255,111,77,0.55)",
}}
>
<Play className="ml-1 h-6 w-6 fill-[#0e0d12]" style={{ color: "#0e0d12" }} />
</span>
<p className="absolute bottom-4 left-4 text-xs font-semibold uppercase tracking-[0.25em] text-white/80">
Watch the 90-second preview
</p>
</button>
)}
</div>
</div>
)}
{/* Event title block */}
<div className="mx-auto mt-6 w-full max-w-[640px] px-5 text-center">
<h1
className="leading-[1.05]"
style={{
fontFamily: "'Fraunces', serif",
fontWeight: 700,
fontSize: "clamp(2.2rem, 6.4vw, 3.2rem)",
color: INK,
letterSpacing: "-0.01em",
}}
>
{event?.title ?? "Featured event"}
</h1>
{event?.subtitle && (
<p className="mt-2 text-base text-white/70">{event.subtitle}</p>
)}
{/* Date + location */}
{event && (
<div className="mt-5 flex flex-wrap items-center justify-center gap-3 text-sm text-white/80">
<span className="inline-flex items-center gap-2">
<Calendar className="h-4 w-4" style={{ color: ACCENT }} />
{formattedDate} · {formattedTime}
</span>
{event.location && (
<span className="inline-flex items-center gap-2">
<MapPin className="h-4 w-4" style={{ color: ACCENT }} />
{event.location}
</span>
)}
</div>
)}
{/* Countdown */}
{event && !done && (
<div className="mx-auto mt-5 grid max-w-[420px] grid-cols-4 gap-2">
<CountdownPill value={days} label="Days" />
<CountdownPill value={hours} label="Hrs" />
<CountdownPill value={mins} label="Min" />
<CountdownPill value={secs} label="Sec" />
</div>
)}
{event && done && (
<p
className="mt-5 inline-block rounded-full px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.22em]"
style={{ background: ACCENT, color: PAPER }}
>
Live now
</p>
)}
{event?.description && (
<p className="mx-auto mt-5 max-w-[460px] text-[15px] leading-relaxed text-white/75">
{event.description}
</p>
)}
{/* RSVP / email capture */}
{event && (
<div className="mx-auto mt-6 w-full max-w-[460px]">
{signedUp ? (
<div
className="flex items-center justify-center gap-2 rounded-2xl border px-4 py-3 text-sm"
style={{ borderColor: RULE, background: CARD, color: INK }}
>
<CheckCircle2 className="h-4 w-4" style={{ color: ACCENT }} />
Saved — calendar invite incoming.
</div>
) : (
<form
onSubmit={(e) => {
e.preventDefault();
if (email) setSignedUp(true);
}}
className="flex flex-col gap-2 sm:flex-row"
>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@somewhere.com"
className="min-w-0 flex-1 rounded-full border bg-transparent px-4 py-3 text-sm text-white placeholder:text-white/40 focus:outline-none"
style={{ borderColor: RULE }}
/>
<button
type="submit"
className="shrink-0 rounded-full px-5 py-3 text-sm font-semibold"
style={{ background: ACCENT, color: PAPER }}
>
{event.ctaLabel ?? "Save your seat"}
</button>
</form>
)}
<p className="mt-2 text-[11px] text-white/45">
Free · 90 minutes · Replay sent if you can't make it live
</p>
</div>
)}
</div>
{/* Speakers / agenda strip */}
<div className="mx-auto mt-10 w-full max-w-[640px] px-5">
<p
className="text-center text-[10px] font-semibold uppercase tracking-[0.32em]"
style={{ color: ACCENT }}
>
What you'll get
</p>
<ul className="mt-4 grid gap-3 sm:grid-cols-3">
{[
{
icon: Sparkles,
title: "The exact morning routine",
body: "I'll walk through the 4-step routine — and the writing prompts I use every day.",
},
{
icon: Sparkles,
title: "Live Q&A",
body: "Bring your questions. I answer every single one for 30 minutes at the end.",
},
{
icon: Sparkles,
title: "Bonus PDF",
body: "Everyone who shows up live gets the workbook ($29 value) — free.",
},
].map((card) => {
const Icon = card.icon;
return (
<li
key={card.title}
className="rounded-2xl border p-4"
style={{ background: CARD, borderColor: RULE }}
>
<Icon className="h-5 w-5" style={{ color: ACCENT }} />
<p className="mt-2 text-sm font-semibold text-white">{card.title}</p>
<p className="mt-1 text-xs leading-relaxed text-white/60">
{card.body}
</p>
</li>
);
})}
</ul>
</div>
{/* Host card */}
<div className="mx-auto mt-10 w-full max-w-[460px] px-5">
<div
className="flex items-center gap-4 rounded-2xl border p-4"
style={{ background: CARD, borderColor: RULE }}
>
<img
src={data.avatar}
alt={data.name}
width={64}
height={64}
loading="lazy"
className="h-16 w-16 rounded-full object-cover"
style={{ border: `2px solid ${ACCENT}` }}
/>
<div className="min-w-0 flex-1">
<p className="text-[10px] font-semibold uppercase tracking-[0.25em] text-white/55">
Hosted by
</p>
<p className="text-base font-semibold text-white">
{data.name}
{data.verified && (
<span className="ml-1.5 align-middle text-xs" style={{ color: ACCENT }}>✓</span>
)}
</p>
{data.credentials && (
<p className="text-xs text-white/65">{data.credentials}</p>
)}
</div>
</div>
{/* Plain links */}
<ul className="mt-5 space-y-2">
{data.links.slice(0, 4).map((link) => {
const Icon = link.icon ? ICONS[link.icon] : ArrowUpRight;
return (
<li key={link.label}>
<a
href={link.href}
className="flex items-center gap-3 rounded-xl border px-4 py-3 transition-colors"
style={{ background: CARD, borderColor: RULE, color: INK }}
>
<Icon className="h-4 w-4 shrink-0" strokeWidth={1.6} />
<span className="flex-1 truncate text-sm">{link.label}</span>
<ArrowUpRight className="h-4 w-4 shrink-0 text-white/40" />
</a>
</li>
);
})}
</ul>
{data.socials && data.socials.length > 0 && (
<div className="mt-7 flex items-center justify-center gap-4 text-white/55">
{data.socials.map((s, i) => {
const Icon = ICONS[s.type] ?? Globe;
return (
<a
key={i}
href={s.href}
aria-label={s.type}
className="rounded-full border p-2 transition-colors hover:text-white"
style={{ borderColor: RULE }}
>
<Icon className="h-4 w-4" strokeWidth={1.6} />
</a>
);
})}
</div>
)}
</div>
</section>
</>
);
} Claude Code Instructions
CLI Install
npx innovations add event-videoWhere to use it
An event-promo link-in-bio page for launching a webinar, cohort, book launch, or live workshop. Loads Fraunces and Inter from Google Fonts.
Pass a typed 'data' prop (LinksInBioData from src/registry/links-in-bio/types.ts). Provide:
- event — { videoEmbed, videoThumb, title, subtitle, startsAt (ISO), location, description, ctaLabel, ctaHref }
- credentials — short host credentials line shown on the host card
The countdown ticks every second based on event.startsAt; when the event is live, it shows a 'Live now' pill instead. The video block is click-to-play (no autoplay) — set videoEmbed to a clean YouTube/Vimeo embed URL and videoThumb to your custom poster image.
In Astro:
import LinksInBioEventVideo from '../components/innovations/links-in-bio/event-video';
<LinksInBioEventVideo client:load data={myProfile} />
In Next.js:
import LinksInBioEventVideo from '@/components/innovations/links-in-bio/event-video';
Best for: live workshops, cohort launches, book launches, webinars, conferences.