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.

Section components are the building blocks of every invitation. Each one represents a single full-height slide in the vertical Splide carousel. No props are passed in from outside — all event-specific data (names, dates, addresses, phone numbers) is hardcoded directly inside the component. This keeps the component tree simple and makes copying/editing an invitation a straightforward find-and-replace operation.
Section components do not need a 'use client' directive individually. They inherit the client context from the top-level page.tsx, which is declared 'use client'. Components that use browser APIs directly — such as AudioControl (useRef, useEffect) or Confirm (window.open) — do include 'use client' as a safeguard, but it is not required for components that only use Framer Motion.

The Header is the first section every guest sees. It occupies a full viewport height, centres the celebrant’s name and portrait photo, and displays the event date. All text and the portrait image animate in on mount using Framer Motion variants.
// src/app/quinces/daniela/components/Header.tsx
import { motion } from "framer-motion";
import { notoSerif, pinyion, playFair } from "./Fonts";
import { animation01, animation02, animation03, animation04 } from "./Animations";

export default function Header() {
  return (
    <section
      className="flex justify-center items-center flex-col"
      style={{ height: "100svh" }}
    >
      <motion.h1
        className={`${playFair.className} text-2xl flex items-center text-zinc-600 z-20`}
        variants={animation01}
        initial="hidden"
        whileInView="visible"
      >
        MIS <span className="text-5xl text-yellow-400">XV</span> AÑOS
      </motion.h1>

      <div className="relative h-72 w-72 flex items-center justify-center mt-5">
        <motion.div
          className="bg-[url('/img/quinces/daniela/fifteen-girl.jpg')] bg-contain bg-no-repeat bg-center absolute w-60 h-60 rounded-full mb-2"
          variants={animation01}
          initial="hidden"
          whileInView="visible"
        />
        <motion.div
          className="bg-[url('/img/quinces/daniela/header-03.png')] bg-contain bg-no-repeat bg-center absolute w-full h-full"
          variants={animation02}
          initial="hidden"
          whileInView="visible"
        />
      </div>

      <motion.p
        className={`${pinyion.className} text-7xl text-yellow-400 mt-5 z-20`}
        style={{ textShadow: "0px 1px 1px rgba(255,255,255, 1)" }}
        variants={animation03}
        initial="hidden"
        whileInView="visible"
      >
        Daniela
      </motion.p>

      <motion.p
        className={`${notoSerif.className} mx-10 mt-1 text-center text-xs z-20 font-bolder tracking-widest text-zinc-900`}
        style={{ textShadow: "0px 1px 1px rgba(255,255,255, 1)" }}
        variants={animation03}
        initial="hidden"
        whileInView="visible"
      >
        CELEBRA CONMIGO EN ESTE DÍA TAN ESPECIAL
      </motion.p>

      <motion.p
        className={`${playFair.className} z-20 flex items-center mt-3 font-bold text-yellow-400`}
        variants={animation04}
        initial="hidden"
        whileInView="visible"
      >
        <span className="border-y-1 border-y-yellow-400 py-2">DOMINGO</span>{" "}
        <span className="text-4xl px-2 pb-2">30</span>{" "}
        <span className="border-y-1 border-y-yellow-400 py-2">OCTUBRE</span>
      </motion.p>
      <motion.p
        className={`${playFair.className} z-20 text-2xl text-yellow-400`}
        variants={animation04}
        initial="hidden"
        whileInView="visible"
      >
        2026
      </motion.p>
    </section>
  );
}
Key patterns:
  • The portrait photo is applied as a CSS background-image on a motion.div using the Tailwind bg-[url(...)] arbitrary-value syntax.
  • A decorative PNG frame overlay (header-03.png) is absolutely positioned on top of the circular portrait.
  • The date is split into three <span> elements so you can style the day number at a larger font size while keeping the month and weekday inline.

Ceremony

The Ceremony section shows the church or ceremony venue. It uses a blurred Card from NextUI as its background, displays an avatar-sized venue photo, and offers a “VER UBICACIÓN” button that opens a Google Maps iframe inside a modal.
// src/app/quinces/daniela/components/Ceremony.tsx (key excerpt)
export default function Ceremony() {
  const { isOpen, onOpen, onOpenChange } = useDisclosure();

  return (
    <>
      <section
        className="flex justify-center items-center flex-col px-7 py-10"
        style={{ height: "100svh" }}
      >
        <Card className="border-none bg-background/5 h-full w-full"
          shadow="sm" radius="lg" isBlurred>
          <CardBody className="flex items-center justify-center flex-col">
            <motion.h1
              className={`${pinyion.className} text-5xl text-yellow-400 mt-5 text-center z-50`}
              variants={animation01} initial="hidden" whileInView="visible"
            >
              Ceremonia
            </motion.h1>

            <Avatar isBordered color="warning"
              src="/img/quinces/daniela/church.jpg"
              className="h-40 w-40 my-5 shadow-lg"
            />

            <motion.p
              className={`${playFair.className} mx-10 mt-1 text-center text-base z-20 text-zinc-900 max-w-md`}
              variants={animation03} initial="hidden" whileInView="visible"
            >
              Catedral Metropolitana de la Inmaculada Concepción
            </motion.p>

            <motion.div className={`mt-3 ${aref.className} p-1 text-center`}
              variants={animation03} initial="hidden" whileInView="visible">
              <p>DOMINGO 30 DE OCTUBRE</p>
              <p className="flex items-center justify-center gap-1">
                <IoMdTime /> 18:00 HRS
              </p>
            </motion.div>

            <Button className="mt-3 mb-4 text-tiny"
              variant="bordered" color="warning" radius="lg" size="sm"
              onClick={() => onOpen()}>
              <LuMapPin /> VER UBICACIÓN
            </Button>
          </CardBody>
        </Card>
      </section>
      <ModalMap isOpen={isOpen} onOpenChange={onOpenChange} />
    </>
  );
}
To customise: replace the venue name, street address, date/time, avatar src, and the <iframe src> inside the MapCeremony sub-component with the new Google Maps embed URL.

Reception

Reception follows the same structure as Ceremony — a blurred card with an avatar venue photo, name, date/time, address, and a map modal. The only difference is the section title (“Recepción”) and the specific venue data.
// src/app/quinces/daniela/components/Reception.tsx (key excerpt)
export default function Reception() {
  const { isOpen, onOpen, onOpenChange } = useDisclosure();

  return (
    <>
      <section
        className="flex justify-center items-center flex-col px-7 py-10"
        style={{ height: "100svh" }}
      >
        <Card className="border-none bg-background/5 h-full w-full"
          shadow="sm" radius="lg" isBlurred>
          <CardBody className="flex items-center justify-center flex-col">
            <motion.h1
              className={`${pinyion.className} text-5xl text-yellow-400 mt-5 text-center z-50`}
              variants={animation01} initial="hidden" whileInView="visible"
            >
              Recepción
            </motion.h1>

            <Avatar isBordered color="warning"
              src="/img/quinces/daniela/salon.jpg"
              className="h-40 w-40 my-5 shadow-lg"
            />

            <motion.p
              className={`${playFair.className} mx-10 mt-1 text-center text-base z-20 text-zinc-900`}
              variants={animation03} initial="hidden" whileInView="visible"
            >
              Salones "Aurora"
            </motion.p>

            <motion.div className={`mt-3 ${aref.className} p-1 text-center`}
              variants={animation03} initial="hidden" whileInView="visible">
              <p>DOMINGO 30 DE OCTUBRE</p>
              <p className="flex items-center justify-center gap-1">
                <IoMdTime /> 20:00 HRS
              </p>
            </motion.div>

            <Button className="mt-3 mb-4 text-tiny"
              variant="bordered" color="warning" radius="lg" size="sm"
              onClick={() => onOpen()}>
              <LuMapPin /> VER UBICACIÓN
            </Button>
          </CardBody>
        </Card>
      </section>
      <ModalMap isOpen={isOpen} onOpenChange={onOpenChange} />
    </>
  );
}

GodParents

GodParents renders a Flowbite Carousel inside a blurred card. Each slide in the carousel represents one godparent pair: a role label (e.g. “Honor”, “Coronación”, “Muñeca”) and the names of the padrinos.
// src/app/quinces/daniela/components/GodParents.tsx (key excerpt)
export default function GodParents() {
  return (
    <section
      className="flex justify-center items-center flex-col px-7 py-10"
      style={{ height: "100svh" }}
    >
      <Card className="border-none bg-background/5 h-full w-full"
        shadow="sm" radius="lg" isBlurred>
        <CardBody className="flex items-center justify-center flex-col overflow-clip">
          <motion.h1
            className={`${pinyion.className} text-5xl text-yellow-400 mt-5 text-center`}
            variants={animation01} initial="hidden" whileInView="visible"
          >
            Padrinos
          </motion.h1>

          <motion.div className="w-full"
            variants={animation03} initial="hidden" whileInView="visible">
            <Carousel theme={customTheme as any}>
              <div className="flex h-full items-center justify-center pb-10 text-gray-800 flex-col px-5 text-center">
                <p className={`${pinyion.className} text-4xl pb-4`}>Honor</p>
                <p className={`${playFair.className}`}>
                  Mariela Macias Rincon <br />&<br /> Ignacio López Lozano
                </p>
              </div>
              <div className="flex h-full items-center justify-center pb-10 text-gray-800 flex-col px-5 text-center">
                <p className={`${pinyion.className} text-4xl pb-4`}>Coronación</p>
                <p className={`${playFair.className}`}>
                  Leticia Barragan Saldaña <br />&<br /> Juan Pablo Robles Castillo
                </p>
              </div>
              <div className="flex h-full items-center justify-center pb-10 text-gray-800 flex-col px-5 text-center">
                <p className={`${greatVibes.className} text-4xl pb-4`}>Muñeca</p>
                <p className={`${playFair.className}`}>
                  Rocio Hernández García <br />&<br /> Pedro Ruíz Sanchez
                </p>
              </div>
            </Carousel>
          </motion.div>
        </CardBody>
      </Card>
    </section>
  );
}
Add a new <div> inside <Carousel> for each godparent pair. There is no hard limit on the number of slides.

DressCode

DressCode communicates the event’s dress requirements. The component from the Gabriela invitation shows separate sections for women and men, each with an SVG clothing icon and a text label, both animated independently.
// src/app/quinces/gabriela/components/DressCode.tsx (key excerpt)
import { motion } from "framer-motion";
import { Card, CardBody } from "@nextui-org/react";
import { greatVibes, notoSans } from "./Fonts";
import { animation01, animation06 } from "./Animations";

export default function DressCode() {
  return (
    <section
      className="flex justify-center items-center flex-col px-7 py-10"
      style={{ height: "100svh" }}
    >
      <Card className="border-none bg-background/5 h-full w-full"
        shadow="sm" radius="lg" isBlurred>
        <CardBody className="flex items-center justify-center flex-col">
          <motion.h1
            className={`${greatVibes.className} text-4xl text-center z-50 p-2 mb-2`}
            variants={animation01} initial="hidden" whileInView="visible"
          >
            Codigo de Vestimenta
          </motion.h1>

          {/* Decorative SVG divider — animated with animation06 */}
          <motion.div variants={animation06} initial="hidden" whileInView="visible">
            {/* inline SVG ornament */}
          </motion.div>

          {/* Women */}
          <div className="flex items-center flex-col justify-center pt-10">
            <motion.div
              initial={{ y: 100, opacity: 0 }}
              whileInView={{ y: 0, opacity: 1 }}
              transition={{ duration: 1, ease: "easeOut", delay: 0.6 }}
            >
              {/* SVG dress icon */}
            </motion.div>
            <motion.h2 className={`${greatVibes.className} pt-5 text-3xl`}
              initial={{ y: 100, opacity: 0 }}
              whileInView={{ y: 0, opacity: 1 }}
              transition={{ duration: 1, ease: "easeInOut", delay: 0.9 }}>
              Para Mujeres
            </motion.h2>
            <motion.p className={`${notoSans.className}`}
              initial={{ y: 100, opacity: 0 }}
              whileInView={{ y: 0, opacity: 1 }}
              transition={{ duration: 1, ease: "easeInOut", delay: 0.9 }}>
              Semi-Formal
            </motion.p>
          </div>

          {/* Men */}
          <div className="flex items-center flex-col justify-center pt-5">
            <motion.div
              initial={{ y: -100, opacity: 0 }}
              whileInView={{ y: 0, opacity: 1 }}
              transition={{ duration: 1, ease: "easeOut", delay: 0.6 }}>
              {/* SVG suit icon */}
            </motion.div>
            <motion.h2 className={`${greatVibes.className} pt-5 text-3xl`}
              initial={{ y: -100, opacity: 0 }}
              whileInView={{ y: 0, opacity: 1 }}
              transition={{ duration: 1, ease: "easeInOut", delay: 0.9 }}>
              Para Caballeros
            </motion.h2>
            <motion.p className={`${notoSans.className}`}
              initial={{ y: -100, opacity: 0 }}
              whileInView={{ y: 0, opacity: 1 }}
              transition={{ duration: 1, ease: "easeInOut", delay: 0.9 }}>
              Semi-Formal
            </motion.p>
          </div>
        </CardBody>
      </Card>
    </section>
  );
}
Replace the text labels (“Semi-Formal”) with the actual dress code for the event. The SVG icons are inline and can be swapped out freely.

Confirm

Confirm is the RSVP section. It provides a WhatsApp deep-link button and a phone-call button so guests can confirm their attendance directly from the invitation. The onClick handler uses window.open to launch the external URL.
// src/app/quinces/daniela/components/Confirm.tsx (key excerpt)
import { motion } from "framer-motion";
import { aref, pinyion } from "./Fonts";
import { animation01, animation03, animation04, animation06 } from "./Animations";
import { Button, Card, CardBody } from "@nextui-org/react";
import { FaPhoneAlt, FaWhatsapp } from "react-icons/fa";

export default function Confirm() {
  return (
    <section
      className="flex items-center justify-center flex-col px-7 py-10"
      style={{ height: "100svh" }}
    >
      <Card className="border-none bg-background/5 h-full w-full"
        shadow="sm" radius="lg" isBlurred>
        <CardBody className="flex items-center justify-center flex-col">
          <motion.h1
            className={`${pinyion.className} text-5xl text-yellow-400 mt-5 text-center`}
            variants={animation01} initial="hidden" whileInView="visible"
          >
            Asistencia
          </motion.h1>

          {/* Decorative SVG divider — animated with animation06 */}
          <motion.div variants={animation06} initial="hidden" whileInView="visible">
            {/* inline SVG ornament */}
          </motion.div>

          <motion.p className={`${aref.className} text-center mt-10 pb-2 px-5 max-w-md`}
            variants={animation03} initial="hidden" whileInView="visible">
            Espero que puedas venir a compartir con nosotros este día inolvidable.
          </motion.p>
          <motion.p className={`${aref.className} text-center p-2`}
            variants={animation03} initial="hidden" whileInView="visible">
            Favor de confirmar tu presencia.
          </motion.p>
          <motion.p className={`${aref.className} text-center p-2`}
            variants={animation04} initial="hidden" whileInView="visible">
            ¡Muchas Gracias!
          </motion.p>

          <motion.div className="pt-5"
            variants={animation04} initial="hidden" whileInView="visible">
            <Button
              variant="bordered"
              color="warning"
              onClick={() => window.open("https://wa.link/llxza9", "_blank")}
              className="w-60"
            >
              <FaWhatsapp className="text-2xl" />
              Mensaje de Whatsapp
            </Button>
          </motion.div>

          <motion.div className="pt-5"
            variants={animation04} initial="hidden" whileInView="visible">
            <Button
              variant="bordered"
              color="warning"
              onClick={() => window.open("tel:5511422546", "_blank")}
              className="w-60"
            >
              <FaPhoneAlt className="text-2xl" />
              Llamada Telefonica
            </Button>
          </motion.div>
        </CardBody>
      </Card>
    </section>
  );
}
Replace the wa.link short URL with a new WhatsApp link for each invitation. Generate short links at wa.link or use the direct format https://wa.me/52XXXXXXXXXX?text=....

AudioControl

AudioControl is a floating button fixed to the bottom-right corner that plays and pauses background music. It uses a useRef on an <audio> element and a useEffect to react to the isPlayed state toggle.
// 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);

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

  return (
    <>
      <motion.button
        type="button"
        className="bg-zinc-900/50 p-3 rounded-full text-white fixed bottom-0 right-0 font-medium shadow-md mb-5 mr-5 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" as const }}
      >
        {isPlayed ? <FaVolumeUp /> : <FaVolumeOff />}
      </motion.button>

      <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>
    </>
  );
}
Place the .mp3 file in public/media/ and update the src path. Note that most browsers require a user gesture before audio can play; the opening modal click serves as that gesture, so autoplay works reliably once the guest taps “Ver Invitación”.
The scroll-snap pattern: every section <section> element must have style={{ height: "100svh" }}. The Splide slider handles scroll-snapping at the container level (height: "100svh", direction: "ttb"), so each slide is exactly one viewport tall. Using svh (small viewport height) instead of vh prevents layout shifts on mobile browsers where the address bar changes height.

Build docs developers (and LLMs) love