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.

Every invitation in this platform lives at a dedicated Next.js app-router route. The workflow is always the same: pick a category namespace, scaffold the directory, create a layout.tsx for SEO metadata, build your section components, and drop your assets into public/. The sections are client components powered by Framer Motion and assembled by a central Main.tsx that wires them into a vertical scroll-snap slider via Splide.
1

Choose the category

Decide which URL namespace the invitation belongs to. The available categories are:
CategoryURL prefixExample
Bodas/bodas/bodas/maria-pedro
Quinceañeras/quinces/quinces/daniela
Bautizos/bautizos/bautizos/mateo
Escolar/escolar/escolar/prepa-2025
Festejos/festejos/festejos/anell-joanha
Create the base directory for your invitation:
mkdir -p src/app/quinces/[name]/components
2

Create the layout file

The layout.tsx file defines the page metadata used by search engines and social-sharing previews. Keep the structure minimal — the layout itself renders only {children}.
// src/app/quinces/[name]/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Daniela | Mis XV Años",
  description:
    "Mis 15 primaveras han llegado y lo que más me llena de emoción es pasarlo con risas a tu lado.",
  openGraph: {
    title: "Daniela | Mis XV Años",
    description:
      "Mis 15 primaveras han llegado y lo que más me llena de emoción es pasarlo con risas a tu lado.",
    images: [
      `https://invitaciones.unaideamas.com/img/quinces/daniela/gallery-03.jpg`,
    ],
  },
  icons: {
    icon: "https://invitaciones.unaideamas.com/img/favicon.png",
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <>{children}</>;
}
Key fields to customise for each invitation:
  • title — shown in browser tab and link previews.
  • description — shown in search results and social cards.
  • openGraph.images — the preview photo (use a portrait gallery image).
  • icons.icon — use the shared platform favicon or a custom one.
3

Create the components directory

Inside src/app/quinces/[name]/components/ you will create at minimum these files:
components/
├── Fonts.ts          # Font declarations (Google + local)
├── Animations.ts     # Framer Motion variant objects
├── Header.tsx        # Full-screen hero section
├── Main.tsx          # Assembles all sections in the Splide slider
├── Presentation.tsx  # Personal message / quote section
├── Ceremony.tsx      # Church / venue details
├── Reception.tsx     # Reception venue details
├── GodParents.tsx    # Godparent carousel
├── Gallery.tsx       # Photo gallery
├── Gifts.tsx         # Gift registry / links
├── Confirm.tsx       # RSVP / WhatsApp confirmation
└── AudioControl.tsx  # Background music toggle
Add or remove sections freely — only Header.tsx, Main.tsx, Fonts.ts, and Animations.ts are required for every invitation.
4

Define fonts

Create components/Fonts.ts and export typed font instances using next/font/google. Here is the complete file from the Daniela invitation as a template:
// src/app/quinces/daniela/components/Fonts.ts
import {
  Great_Vibes,
  Playfair_Display,
  Pinyon_Script,
  Noto_Serif_Balinese,
  Roboto,
  Titillium_Web,
  Oswald,
  Clicker_Script,
  Aref_Ruqaa,
  Rajdhani,
  Yeseva_One,
} from "next/font/google";

export const greatVibes = Great_Vibes({ subsets: ["latin"], weight: "400" });
export const roboto = Roboto({ subsets: ["latin"], weight: ["400"] });
export const pinyion = Pinyon_Script({ subsets: ["latin"], weight: ["400"] });
export const notoSerif = Noto_Serif_Balinese({
  subsets: ["latin"],
  weight: ["400"],
});
export const playFair = Playfair_Display({
  subsets: ["latin"],
  weight: ["400", "600", "700"],
});
export const notoSans = Titillium_Web({ subsets: ["latin"], weight: "400" });
export const oswald = Oswald({ subsets: ["latin"], weight: "400" });
export const clicker = Clicker_Script({ subsets: ["latin"], weight: "400" });
export const aref = Aref_Ruqaa({ subsets: ["latin"], weight: "400" });
export const rajdhani = Rajdhani({ subsets: ["latin"], weight: "400" });
export const yaseva = Yeseva_One({ subsets: ["latin"], weight: "400" });
See Fonts & Styling for the full guide, including how to load custom .otf/.ttf files from public/fonts/.
5

Define animations

Create components/Animations.ts with named Framer Motion variant objects. Each section imports only the variants it needs.
// src/app/quinces/daniela/components/Animations.ts
export const animation01 = {
  hidden: { opacity: 0, scale: 0 },
  visible: {
    opacity: 1,
    scale: 1,
    transition: { duration: 1, type: "spring" as const, stiffness: 70, delay: 0.4 },
  },
};

export const animation02 = {
  hidden: { y: 100, rotate: 180, opacity: 0 },
  visible: {
    y: 0,
    rotate: 0,
    opacity: 1,
    transition: { duration: 1 },
  },
};

export const animation03 = {
  hidden: { y: 100, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: { duration: 1, delay: 0.8 },
  },
};

export const animation04 = {
  hidden: { y: -100, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: { duration: 1, delay: 1 },
  },
};

export const animation05 = {
  hidden: { scale: 2, opacity: 0 },
  visible: {
    scale: 1,
    opacity: 1,
    transition: { duration: 2 },
  },
};

export const animation06 = {
  hidden: { opacity: 0, scale: 0 },
  visible: {
    opacity: 1,
    scale: 1,
    transition: { duration: 1, delay: 0.6 },
  },
};
Use animation01animation06 as-is or add your own numbered variants following the same pattern.
6

Create Main.tsx

Main.tsx is the heart of the invitation. It imports every section component and renders them as individual slides inside a Splide vertical slider, with AudioControl floating above. It also shows a brief swipe-hint modal on first mount, then fades in the slider once the hint dismisses.
// src/app/quinces/daniela/components/Main.tsx
import { useEffect, useState } from "react";
// @ts-ignore
import { Splide, SplideSlide } from "@splidejs/react-splide";
import Header from "./Header";
import Presentation from "./Presentation";
import Ceremony from "./Ceremony";
import Reception from "./Reception";
import GodParents from "./GodParents";
import Gallery from "./Gallery";
import Gifts from "./Gifts";
import Confirm from "./Confirm";
import { Modal, ModalBody, ModalContent, useDisclosure } from "@nextui-org/react";
import { motion } from "framer-motion";
import { oswald } from "./Fonts";
import "@splidejs/react-splide/css";
import { animation05 } from "./Animations";
import AudioControl from "./AudioControl";

const ModalInstructions = ({
  isOpen,
  onOpenChange,
}: {
  isOpen: boolean;
  onOpenChange: () => void;
}) => (
  <Modal
    isOpen={isOpen}
    onOpenChange={onOpenChange}
    size="xs"
    placement="center"
    backdrop="blur"
    className="bg-white/0 shadow-none"
    hideCloseButton={true}
    isDismissable={false}
  >
    <ModalContent>
      {() => (
        <ModalBody>
          {/* animated swipe arrow SVG */}
          <p className={`${oswald.className} text-white text-2xl text-center mt-5`}>
            DESLIZA PARA VER EL CONTENIDO
          </p>
        </ModalBody>
      )}
    </ModalContent>
  </Modal>
);

export default function Main() {
  const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
  const [open, setOpen] = useState(false);

  useEffect(() => {
    onOpen();
    setTimeout(() => {
      onClose();
      setOpen(true);
    }, 2000);
  }, []);

  return (
    <div className="max-w-3xl m-auto bg-[url('/img/quinces/[name]/background-main.jpg')] bg-center bg-cover bg-fixed shadow-large">
      {open && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          transition={{ duration: 1 }}
          className="relative overflow-clip"
        >
          <Splide
            aria-label="[Name]"
            options={{
              rewind: true,
              direction: "ttb",
              height: "100svh",
              wheel: true,
              releaseWheel: true,
              type: "loop",
              waitForTransition: true,
              arrows: false,
              classes: {
                page: "splide__pagination__page custom-class-page",
              },
            }}
            className="z-10"
          >
            <SplideSlide><Header /></SplideSlide>
            <SplideSlide><Presentation /></SplideSlide>
            <SplideSlide><Ceremony /></SplideSlide>
            <SplideSlide><Reception /></SplideSlide>
            <SplideSlide><GodParents /></SplideSlide>
            <SplideSlide><Gallery /></SplideSlide>
            <SplideSlide><Gifts /></SplideSlide>
            <SplideSlide><Confirm /></SplideSlide>
          </Splide>
          <AudioControl />
          <motion.div
            className="bg-[url('/img/quinces/[name]/header-01.png')] bg-cover bg-bottom absolute inset-0 z-0"
            variants={animation05}
            initial="hidden"
            whileInView="visible"
          />
          <motion.div
            className="bg-[url('/img/quinces/[name]/header-02.png')] bg-cover bg-bottom absolute inset-0 z-0"
            variants={animation05}
            initial="hidden"
            whileInView="visible"
          />
        </motion.div>
      )}
      <ModalInstructions isOpen={isOpen} onOpenChange={onOpenChange} />
    </div>
  );
}
Key points:
  • ModalInstructions is an internal modal that auto-dismisses after 2 seconds, prompting the guest to swipe.
  • The open state gate ({open && ...}) ensures the Splide slider only mounts after the hint dismisses.
  • classes.page passes a custom CSS class to Splide pagination dots, styled via styles.css.
  • The two decorative motion.div overlays (header-01.png, header-02.png) sit behind the slider at z-0.
  • Adjust the SplideSlide list to match the sections you created. Remove slides for sections you skipped.
7

Create page.tsx

page.tsx is the entry point for the route. It renders an opening modal (ModalOpening) that shows the guest a preview of the invitation name, then — once they tap “Ver Invitación” — mounts <Main />. It also imports ./styles.css for per-invitation overrides.
// src/app/quinces/[name]/page.tsx
"use client";

import { motion } from "framer-motion";
import {
  Button, Modal, ModalBody, ModalContent,
  ModalFooter, ModalHeader, useDisclosure,
} from "@nextui-org/react";
import { useEffect, useState, Dispatch, SetStateAction } from "react";
import { pinyion, playFair } from "./components/Fonts";
import { animation01, animation03 } from "./components/Animations";
import "./styles.css";
import Main from "./components/Main";

const ModalOpening = ({
  isOpen, onOpenChange, setOpen,
}: {
  isOpen: boolean;
  onOpenChange: () => void;
  setOpen: Dispatch<SetStateAction<boolean>>;
}) => (
  <Modal isOpen={isOpen} onOpenChange={onOpenChange}
    size="xs" placement="center" backdrop="blur"
    isDismissable={false} hideCloseButton={true}>
    <ModalContent>
      {(onClose) => (
        <>
          <ModalHeader className="flex flex-col gap-1">
            <motion.h1
              className={`${playFair.className} text-2xl flex items-center justify-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>
          </ModalHeader>
          <ModalBody>
            <motion.p
              className={`${pinyion.className} flex justify-center text-7xl text-yellow-400 z-20`}
              style={{ textShadow: "0px 1px 1px rgba(255,255,255, 1)" }}
              variants={animation03} initial="hidden" whileInView="visible"
            >
              Daniela
            </motion.p>
          </ModalBody>
          <ModalFooter className="flex justify-center">
            <Button color="warning" variant="bordered"
              onPress={() => { setOpen(true); onClose(); }}>
              Ver Invitación
            </Button>
          </ModalFooter>
        </>
      )}
    </ModalContent>
  </Modal>
);

export default function Fifteen() {
  const [open, setOpen] = useState(false);
  const { isOpen, onOpen, onOpenChange } = useDisclosure();

  useEffect(() => { onOpen(); }, []);

  return (
    <main className="background-class">
      {open && <Main />}
      <ModalOpening isOpen={isOpen} onOpenChange={onOpenChange} setOpen={setOpen} />
    </main>
  );
}
8

Add assets

Place image assets in public/img/[category]/[name]/ and any custom font files in public/fonts/. See Assets & Images for full naming conventions and sizing recommendations.
public/
├── fonts/
│   └── MyCustomFont.otf
└── img/
    └── quinces/
        └── [name]/
            ├── header.jpg
            ├── background-main.jpg
            ├── gallery-01.jpg
            ├── gallery-02.jpg
            └── preview.jpg
9

Run the dev server

Start the local development server and open your invitation:
npm run dev
Then visit:
http://localhost:3000/quinces/[name]
The opening modal will appear immediately. Tap “Ver Invitación” to enter the scrollable invitation. Use your browser’s device-simulation mode to preview at mobile widths (375–430 px), since the layout is optimised for portrait phones.
The fastest way to create a new invitation is to duplicate an existing one that shares the same event type and color palette, then replace the asset paths, text strings, venue details, and font selections. The Daniela invitation (src/app/quinces/daniela/) is a well-structured reference with all standard sections.
All <Image> components from next/image must use the unoptimized prop. This is configured globally in next.config.js via images: { unoptimized: true } — required because the platform is a static export and the built-in Next.js image optimisation API is not available on static hosts.

Build docs developers (and LLMs) love