hover-grid HLS MP4
Desktop portfolio: clips play on hover, and a click opens a lightbox that continues from the clip
import { preload, useVideo, Video } from "@uln/hls/react";
import { useEffect, useState } from "react";
import { type Film, fetchFilms } from "../films";
/** Desktop portfolio: posters that play their clip on hover; a click opens the film from that moment. */
export function App() {
const [films, setFilms] = useState<Film[]>([]);
const [open, setOpen] = useState<{ film: Film; start: number } | null>(null);
useEffect(() => {
void fetchFilms(0).then((page) => {
setFilms(page);
// Warm the first row so the first hovers are instant.
preload(page.slice(0, 4).map((f) => f.clip?.url));
});
}, []);
return (
<>
<ul className="grid">
{films.map((f) => (
<li key={f.id}>
<Tile film={f} onOpen={(start) => setOpen({ film: f, start })} />
</li>
))}
</ul>
{open && <Lightbox film={open.film} start={open.start} onClose={() => setOpen(null)} />}
</>
);
}
function Tile({ film, onOpen }: { film: Film; onOpen: (start: number) => void }) {
const clip = useVideo();
const [hovered, setHovered] = useState(false);
const filmTime = () => (film.clip ? film.clip.start + clip.currentTime : 0);
return (
<button
type="button"
onPointerEnter={() => setHovered(true)}
onPointerLeave={() => setHovered(false)}
onClick={() => onOpen(filmTime())}
>
{film.clip ? (
<Video
src={film.clip.url}
poster={film.poster}
player={clip}
playing={hovered}
muted
loop
// Hovering is a hint: get the film ready to open from here.
onPlay={() => preload(film.hls, { start: filmTime() })}
/>
) : (
<img src={film.poster} alt="" />
)}
<span>{film.title}</span>
</button>
);
}
function Lightbox({ film, start, onClose }: { film: Film; start: number; onClose: () => void }) {
const player = useVideo();
return (
<dialog open className="lightbox">
<Video src={film.hls} poster={film.poster} player={player} playing start={start}>
{/* Sound without a fresh tap can be refused (Safari); then ask for one. */}
{player.status === "blocked" && (
<button type="button" onClick={player.play}>
Play
</button>
)}
</Video>
<button type="button" onClick={onClose}>
Close
</button>
</dialog>
);
}