Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/gus-16710/invitations/llms.txt

Use this file to discover all available pages before exploring further.

Multimedia is a key part of what makes digital invitations feel premium. The platform supports four distinct multimedia capabilities: photo galleries with a full-screen lightbox, background music with a toggle button, video backgrounds embedded per-invitation, and a canvas confetti burst effect on the homepage. Each feature is implemented with a dedicated library that is lightweight and mobile-friendly.

Photo Galleries

The Gallery component uses a two-library combination to deliver a responsive masonry grid with a full-screen lightbox.
LibraryRole
react-photo-albumRenders the responsive masonry grid layout
yet-another-react-lightboxFull-screen lightbox with swipe, zoom, and keyboard support
The photo array is defined statically in the component file. Each entry carries the image src path (relative to /public) and the intrinsic width / height so that react-photo-album can calculate the masonry layout without cumulative layout shift.
// src/app/bodas/diana-ernesto/components/Gallery.tsx (excerpt)
import PhotoAlbum from "react-photo-album";
import "react-photo-album/styles.css";
import Lightbox from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css";
import NextJsImage from "../../vanessa-susana/components/NextJsImage";

const images = [
  { src: "/img/bodas/diana-ernesto/gallery-01.jpg", width: 800,  height: 600 },
  { src: "/img/bodas/diana-ernesto/gallery-02.jpg", width: 1600, height: 900 },
  { src: "/img/bodas/diana-ernesto/gallery-03.jpg", width: 800,  height: 600 },
  // ... more photos
];

export default function Gallery() {
  const [index, setIndex] = useState(-1);

  return (
    <section style={{ height: "100svh" }}>
      {/* Masonry grid — tapping a photo sets the lightbox index */}
      <div className="w-72 mx-auto">
        <PhotoAlbum
          layout="masonry"
          photos={images}
          onClick={({ index: current }) => setIndex(current)}
          render={{ photo: NextJsImage }}   // uses next/image for optimised delivery
          columns={2}
        />
      </div>

      {/* Lightbox opens when index >= 0 */}
      <Lightbox
        index={index}
        slides={images}
        open={index >= 0}
        close={() => setIndex(-1)}
        controller={{
          closeOnBackdropClick: true,
          closeOnPullDown: true,
        }}
      />
    </section>
  );
}
NextJsImage is a thin wrapper component that renders next/image inside react-photo-album’s render slot. This lets the photo album use next/image for lazy loading and responsive sizing while still integrating with the lightbox index system.

Audio Control

Background music is one of the most requested features for XV años and wedding invitations. The AudioControl component auto-plays a .mp3 file stored under /public/media/ and exposes a single toggle button so guests can mute the music at any time.
// src/app/quinces/daniela/components/AudioControl.tsx
import { FaVolumeUp, FaVolumeOff } from "react-icons/fa";
import { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";

export default function AudioControl() {
  const [isPlayed, setIsPlayed] = useState(true);
  const audioPlayer = useRef<HTMLAudioElement>(null);

  // Play or pause whenever isPlayed changes
  useEffect(() => {
    if (isPlayed) {
      audioPlayer.current?.play();
    } else {
      audioPlayer.current?.pause();
    }
  }, [isPlayed]);

  return (
    <>
      {/* Floating toggle button — animates in from above after 1 s delay */}
      <motion.button
        type="button"
        className="bg-zinc-900/50 p-3 rounded-full text-white fixed bottom-0 right-0 z-10 ..."
        onClick={() => setIsPlayed(!isPlayed)}
        initial={{ opacity: 0, scale: 0, y: -100 }}
        animate={{ opacity: 1, scale: 1, y: 0 }}
        transition={{ duration: 1, delay: 1, ease: "anticipate" }}
      >
        {isPlayed ? <FaVolumeUp /> : <FaVolumeOff />}
      </motion.button>

      {/* Hidden HTML audio element */}
      <audio controls ref={audioPlayer} hidden>
        <source src="/media/mi_princesa_angel_melo.mp3" type="audio/mpeg" />
        Your browser does not support the audio element.
      </audio>
    </>
  );
}
Key implementation details:
  • isPlayed starts as true, so the audio attempts to play immediately on mount. Most mobile browsers block auto-play until the user interacts with the page — the component relies on the Splide swipe-hint modal (which requires a tap to dismiss) to satisfy the browser’s user-interaction requirement before play() is called.
  • The button is rendered with position: fixed outside the Splide slider container, so it persists visibly across all slides.
  • The button’s own entrance animation uses Framer Motion’s animate (not whileInView) with a 1-second delay, so it slides into view shortly after the invitation loads.
Each invitation stores its background track in /public/media/. The filename and format are hardcoded per invitation during the customisation process. Supported format is .mp3 (audio/mpeg).

Video Backgrounds

Some invitations use short .mp4 clips as atmospheric backgrounds. The video file is placed in /public/media/ (or /public/img/<event-slug>/) and rendered as a native HTML <video> element positioned absolutely behind the section content.
// Pattern used in video-background sections
<section style={{ height: "100svh", position: "relative", overflow: "hidden" }}>
  <video
    autoPlay
    muted
    loop
    playsInline
    style={{
      position: "absolute",
      inset: 0,
      width: "100%",
      height: "100%",
      objectFit: "cover",
      zIndex: 0,
    }}
  >
    <source src="/media/background.mp4" type="video/mp4" />
  </video>

  {/* Section content sits above the video */}
  <div style={{ position: "relative", zIndex: 1 }}>
    {/* ... */}
  </div>
</section>
autoPlay, muted, and playsInline are all required for the video to auto-play on iOS Safari without user interaction. loop keeps the clip cycling indefinitely.
Large .mp4 files significantly increase the invitation’s initial load time, especially on mobile connections. Keep video background files under 5 MB. Aim for 1–2 MB by encoding at a lower bitrate (e.g., CRF 28–32 in H.264) and trimming clips to 10–15 seconds before looping.

Canvas Confetti

The homepage (src/app/page.tsx) uses react-canvas-confetti to fire a multi-burst confetti explosion on load and then repeat every 10 seconds.
// src/app/page.tsx (excerpt)
import ReactCanvasConfetti from "react-canvas-confetti";
import type { TCanvasConfettiInstance } from "react-canvas-confetti/dist/types";
import { useCallback, useEffect, useRef } from "react";

export default function HomePage() {
  const refAnimationInstance = useRef<TCanvasConfettiInstance | null>(null);

  // Store the confetti instance when the canvas mounts
  const getInstance = useCallback(
    ({ confetti: instance }: { confetti: TCanvasConfettiInstance }) => {
      refAnimationInstance.current = instance;
    },
    []
  );

  // Helper: fire a single shot with given ratio and options
  const makeShot = useCallback((particleRatio: number, opts: object) => {
    refAnimationInstance.current?.({
      ...opts,
      origin: { y: 0.7 },
      particleCount: Math.floor(200 * particleRatio),
    });
  }, []);

  // Compose multiple shots for a realistic burst effect
  const fire = useCallback(() => {
    makeShot(0.25, { spread: 26,  startVelocity: 55 });
    makeShot(0.2,  { spread: 60 });
    makeShot(0.35, { spread: 100, decay: 0.91, scalar: 0.8 });
    makeShot(0.1,  { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 });
    makeShot(0.1,  { spread: 120, startVelocity: 45 });
  }, [makeShot]);

  // Fire on mount, then repeat every 10 seconds
  useEffect(() => {
    fire();
    const timer = setInterval(() => fire(), 10000);
    return () => clearInterval(timer);
  }, []);

  return (
    <>
      {/* Canvas is positioned fixed and covers the full viewport */}
      <ReactCanvasConfetti
        onInit={getInstance}
        style={{
          position: "fixed",
          pointerEvents: "none",
          width: "100%",
          height: "100%",
          top: 0,
          left: 0,
          zIndex: 999,
        }}
      />
      {/* ... rest of homepage */}
    </>
  );
}
The fire() function calls makeShot five times with different spread, startVelocity, decay, and scalar values to produce a natural, multi-directional burst rather than a single uniform spray. The origin: { y: 0.7 } option launches particles from 70% down the viewport, mimicking a party popper fired from waist height.
next/image requires unoptimized: true in next.config.js because the app uses output: "export" for static generation. Without this flag, next/image will throw a build error since its on-demand optimisation server is not available in static export mode.
// next.config.js
const nextConfig = {
  output: "export",
  images: {
    unoptimized: true,
  },
};

Build docs developers (and LLMs) love