Documentation Index
Fetch the complete documentation index at: https://mintlify.com/luigitemu/pikante-landing/llms.txt
Use this file to discover all available pages before exploring further.
PrepVideo.jsx is the only React component in the Pikanté landing. It renders a looping preparation video alongside a branded aside panel, and ships full play/pause and mute/unmute controls. Because it is a React island, it must be used with an Astro client:* directive.
Partial Hydration — client:visible
In Hero.astro the component is consumed as:
<PrepVideo client:visible />
client:visible defers hydration until the component scrolls into the browser viewport, rather than hydrating immediately on page load like client:load would. This means:
- The initial HTML/CSS paint is unblocked — the hero section is interactive before the video island loads.
- The video JavaScript bundle (React + component) is only downloaded and executed when the user actually reaches that part of the page, reducing the initial JS payload for visitors who never scroll.
- The
IntersectionObserver inside the component then takes over to further pause/play the video based on continued visibility.
Use client:load only if the video is above the fold and must autoplay immediately on first render.
Video source & attributes
The video file is served as a static asset from public/assets/:
<video
ref={ref}
src="/assets/prep-video.mp4"
autoPlay
muted
loop
playsInline
preload="metadata"
/>
| Attribute | Purpose |
|---|
autoPlay | Starts playback as soon as the component hydrates |
muted | Required by browsers to allow autoplay without a user gesture |
loop | Restarts automatically when the clip ends |
playsInline | Prevents iOS Safari from opening fullscreen on play |
preload="metadata" | Fetches only duration/dimensions before play, keeping initial load light |
The video file must be placed at public/assets/prep-video.mp4 in the project root. Astro copies everything inside public/ verbatim to the build output, making the file available at the URL path /assets/prep-video.mp4.
IntersectionObserver
A useEffect hook wires up an IntersectionObserver that pauses the video when it leaves the viewport (threshold: 25 % visibility) and resumes it when it re-enters — but only if the user has not manually paused via the play button:
useEffect(() => {
const v = ref.current;
if (!v) return;
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting && playing) {
v.play().catch(() => {});
} else {
v.pause();
}
});
},
{ threshold: 0.25 }
);
io.observe(v);
return () => io.disconnect();
}, [playing]);
The playing state variable is a dependency: re-registering the observer whenever playing changes ensures the visibility callback always has the latest user intent.
State & controls
The component tracks two boolean state values:
| State | Initial | Meaning |
|---|
playing | true | Whether the video should be playing |
muted | true | Whether the video is muted |
Play / Pause
Clicking the play button toggles video.play() / video.pause() and flips playing:
const toggle = () => {
const v = ref.current;
if (!v) return;
if (v.paused) {
v.play().catch(() => {});
setPlaying(true);
} else {
v.pause();
setPlaying(false);
}
};
The button renders a pause icon (two rectangles) when playing is true, and a play icon (triangle) when false.
Mute / Unmute
Clicking the volume button directly toggles video.muted on the DOM element and syncs the muted state:
const toggleMute = () => {
const v = ref.current;
if (!v) return;
v.muted = !v.muted;
setMuted(v.muted);
};
The button renders a volume-x (Lucide) SVG when muted, and a volume-2 SVG when unmuted.
Layout
.prep-video is a two-column CSS grid:
.prep-video {
display: grid;
grid-template-columns: minmax(0, .85fr) 1fr;
gap: 28px;
margin-bottom: 48px;
align-items: stretch;
width: 100%;
}
| Column | Element | Contents |
|---|
Left (.85fr) | .prep-video-frame | <video> + overlay with tag pill + controls |
Right (1fr) | .prep-video-side (aside) | Mono label, <h3>, description paragraph |
The video frame is aspect-ratio: 9/16 with a max-height: 720px cap, giving it a portrait/phone-reel feel. At viewports below 880 px the grid collapses to a single column.
Aside content
<aside className="prep-video-side">
<span className="mono accent">Party · Friends · Vibes</span>
<h3>
Mírala
<br />
<em>en acción.</em>
</h3>
<p>
15 segundos son suficientes para que te prepares la mejor michelada , Pikanté.
</p>
</aside>
To replace the video, update the src attribute on the <video> element in PrepVideo.jsx — e.g. src="/assets/new-video.mp4" — and drop the new file into public/assets/. No other code changes are required; the controls and IntersectionObserver will work with any video source.
Complete JSX source
import { useEffect, useRef, useState } from 'react';
export default function PrepVideo() {
const ref = useRef(null);
const [playing, setPlaying] = useState(true);
const [muted, setMuted] = useState(true);
useEffect(() => {
const v = ref.current;
if (!v) return;
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting && playing) {
v.play().catch(() => {});
} else {
v.pause();
}
});
},
{ threshold: 0.25 }
);
io.observe(v);
return () => io.disconnect();
}, [playing]);
const toggle = () => {
const v = ref.current;
if (!v) return;
if (v.paused) {
v.play().catch(() => {});
setPlaying(true);
} else {
v.pause();
setPlaying(false);
}
};
const toggleMute = () => {
const v = ref.current;
if (!v) return;
v.muted = !v.muted;
setMuted(v.muted);
};
return (
<div className="prep-video reveal">
<div className="prep-video-frame">
<video
ref={ref}
src="/assets/prep-video.mp4"
autoPlay
muted
loop
playsInline
preload="metadata"
/>
<div className="prep-video-overlay">
<span className="prep-video-tag">Reel · 00:08 al primer trago</span>
<div className="prep-video-controls">
<button
onClick={toggle}
aria-label={playing ? 'Pausar' : 'Reproducir'}
className="pv-btn"
type="button"
>
{playing ? (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
) : (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M7 4l14 8-14 8z" />
</svg>
)}
</button>
<button
onClick={toggleMute}
aria-label={muted ? 'Activar sonido' : 'Silenciar'}
className="pv-btn"
type="button"
>
{muted ? (
/* volume-x icon */
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/>
<line x1="22" x2="16" y1="9" y2="15"/>
<line x1="16" x2="22" y1="9" y2="15"/>
</svg>
) : (
/* volume-2 icon */
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/>
<path d="M16 9a5 5 0 0 1 0 6"/>
<path d="M19.364 18.364a9 9 0 0 0 0-12.728"/>
</svg>
)}
</button>
</div>
</div>
</div>
<aside className="prep-video-side">
<span className="mono accent">Party · Friends · Vibes</span>
<h3>
Mírala
<br />
<em>en acción.</em>
</h3>
<p>
15 segundos son suficientes para que te prepares la mejor michelada , Pikanté.
</p>
</aside>
</div>
);
}