Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/spooky-developer/llms.txt

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

The Contact page — titled “Séance” — invites visitors to “reach out to the other side”. It pairs a dark glassmorphism form with a theatrical success state: once a message is submitted, a PumpkinIcon ghost animates up from the bottom of the screen and announces “Boo! Message sent.” in a speech bubble. The entire submission flow is self-contained in the component with a simulated delay, making it trivial to swap in a real backend.

Route

/contact

Form Layout

The form sits inside a glassmorphism container centred on the page (max-w-2xl mx-auto):
<div className="bg-haunt-dark/50 p-8 rounded-2xl border border-haunt-moon/20 backdrop-blur-sm relative z-10">
CSS propertyValueEffect
bg-haunt-dark/5050 % opacity dark fillTransparent dark glass
backdrop-blur-smSmall blurFrosted-glass blur of background
border border-haunt-moon/20Teal border at 20 % opacitySubtle glowing frame

Form Fields

All three fields are labelled with spooky copy (font-spooky text-xl text-haunt-pumpkin) and use consistent input styling (bg-haunt-bg border border-haunt-tombstone rounded p-3 text-haunt-bone focus:border-haunt-moon):
LabelTypePlaceholder
Whisper your nametextJohn Doe
Where shall I haunt you?emailjohn@example.com
Your message from the beyondtextarea (4 rows)I summon thee for a job...
All three fields carry the required attribute, so native browser validation will prevent an empty submission.

Submission Flow

Form submission is handled by handleSubmit, which calls e.preventDefault() to suppress the default browser navigation and then drives a three-state machine stored in local component state:
"idle"  →  "submitting"  →  "success"  →  (5 000 ms)  →  "idle"
const handleSubmit = (e) => {
  e.preventDefault();
  setState('submitting');

  // ── Replace this block with a real fetch call ──
  setTimeout(() => {
    setState('success');
    setTimeout(() => setState('idle'), 5000);
  }, 1500);
  // ──────────────────────────────────────────────
};
StateSubmit button textButton disabled?
idleSend MessageNo
submittingSummoning…Yes (disabled + opacity-50)
successSend MessageNo (form is hidden by success overlay)

Connecting to a Real Backend

Replace the inner setTimeout with a fetch call to your API endpoint:
const handleSubmit = async (e) => {
  e.preventDefault();
  setState('submitting');

  try {
    await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, email, message }),
    });
    setState('success');
    setTimeout(() => setState('idle'), 5000);
  } catch (err) {
    console.error(err);
    setState('idle'); // or add an 'error' state
  }
};

Haunted Success Animation

When state === 'success' and isHaunted is true (read from HauntContext), a full-screen overlay ghost animates up from below the viewport:
<AnimatePresence>
  {state === 'success' && isHaunted && (
    <motion.div
      initial={{ y: 200, opacity: 0 }}
      animate={{ y: 0, opacity: 1 }}
      exit={{ y: 200, opacity: 0 }}
      transition={{ type: 'spring', bounce: 0.5 }}
      className="fixed bottom-0 left-1/2 -translate-x-1/2 z-[9999]
                 flex flex-col items-center pointer-events-none"
    >
      {/* Speech bubble */}
      <div className="bg-haunt-bone text-haunt-dark px-6 py-3 rounded-2xl mb-4 relative font-bold text-lg shadow-2xl">
        Boo! Message sent.
        <div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-4 h-4 bg-haunt-bone rotate-45" />
      </div>

      <PumpkinIcon className="w-32 h-32 text-haunt-pumpkin drop-shadow-[0_0_30px_rgba(251,146,60,0.8)]" />
    </motion.div>
  )}
</AnimatePresence>
The animation plays as a spring (type: 'spring', bounce: 0.5) so the ghost bounces into frame rather than sliding linearly. The speech bubble sits above the pumpkin icon, connected by a small rotated square that acts as a pointer. The success state automatically resets to idle after 5 000 ms, which also triggers AnimatePresence to unmount the ghost with the exit animation (y: 200, opacity: 0) so it slides back below the screen cleanly.
AnimatePresence (imported from assets/index.js) is what makes the exit animation possible. Without it, React would unmount the element immediately when isHaunted && state === 'success' becomes false, and the exit props would never run. Always keep the ghost wrapped in AnimatePresence if you refactor this component.

Haunt Mode Dependency

The success ghost only appears when Haunt Mode is enabled (isHaunted === true). If a visitor has toggled Haunt Mode off via the navigation toggle, submitting the form still works — they simply see the button return to its idle state without the visual flourish. See HauntContext for details on the isHaunted flag and how it is persisted to localStorage.

Build docs developers (and LLMs) love