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
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 property | Value | Effect |
|---|
bg-haunt-dark/50 | 50 % opacity dark fill | Transparent dark glass |
backdrop-blur-sm | Small blur | Frosted-glass blur of background |
border border-haunt-moon/20 | Teal border at 20 % opacity | Subtle 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):
| Label | Type | Placeholder |
|---|
| Whisper your name | text | John Doe |
| Where shall I haunt you? | email | john@example.com |
| Your message from the beyond | textarea (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);
// ──────────────────────────────────────────────
};
| State | Submit button text | Button disabled? |
|---|
idle | Send Message | No |
submitting | Summoning… | Yes (disabled + opacity-50) |
success | Send Message | No (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.