@uln/hls 0.3.1 / examples / hover-grid

hover-grid HLS MP4

Desktop portfolio: clips play on hover, and a click opens a lightbox that continues from the clip

examples/hover-grid/App.tsx

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>
  );
}