Skip to main content

Documentation Index

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

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

FloatingGhost displays a single testimonial — a quote, reviewer name, and job title — beneath a hand-crafted ghost SVG avatar that bobs endlessly up and down using Framer Motion’s infinite animation loop. A soft shadow below the ghost pulses in sync, selling the illusion of gentle levitation. Cards are staggered into view on the Testimonials page using each ghost’s individual delay value.

Props

testimonial
object
required
The testimonial data object to render inside this component.
testimonial.name
string
required
The reviewer’s full name, displayed in bold beneath the quote.
testimonial.role
string
required
The reviewer’s job title and/or company (e.g. "CEO, BloodBank Inc."), rendered in small monospace text below the name.
testimonial.quote
string
required
The testimonial body text. Displayed in the spooky font, wrapped in curly quotes, inside the scroll-shaped card.
testimonial.delay
number
required
Framer Motion entrance delay in seconds. Controls when this card fades up into view relative to the page load, creating a staggered appearance across the three testimonials.

Testimonials Data

The three built-in testimonials are defined in assets/main.js and mapped to individual <FloatingGhost> components on the Testimonials page:
const testimonials = [
  {
    name: 'Dr. Frankenstein',
    role: 'Lead Scientist',
    quote: "It's alive! The code is beautifully structured and monstrously fast.",
    delay: 0,
  },
  {
    name: 'Count Dracula',
    role: 'CEO, BloodBank Inc.',
    quote: 'I love working with them. They never sleep, just like me.',
    delay: 1.5,
  },
  {
    name: 'The Mummy',
    role: 'Product Manager',
    quote: 'Unwrapped our legacy code and brought it into the modern era.',
    delay: 0.8,
  },
];

Ghost Avatar

The ghost is a custom inline SVG (not a Lucide icon) built from a single filled path and several facial features:
<svg viewBox="0 0 100 120" className="w-full h-full">
  {/* Body — wavy hem at the bottom */}
  <path
    d="M 20 100 C 20 40, 30 10, 50 10 C 70 10, 80 40, 80 100
       C 80 110, 75 115, 70 110 C 65 105, 60 115, 55 110
       C 50 105, 45 115, 40 110 C 35 105, 30 115, 25 110
       C 20 115, 20 105, 20 100 Z"
    fill="#f4f1ea"
    opacity="0.9"
  />
  {/* Eyes */}
  <circle cx="40" cy="45" r="5" fill="#0b3a44" />
  <circle cx="60" cy="45" r="5" fill="#0b3a44" />
  {/* Mouth */}
  <ellipse cx="50" cy="65" rx="8" ry="12" fill="#0b3a44" />
  {/* Arms */}
  <path d="M 30 75 Q 20 85 35 90" stroke="#f4f1ea" strokeWidth="6" strokeLinecap="round" fill="none" />
  <path d="M 70 75 Q 80 85 65 90" stroke="#f4f1ea" strokeWidth="6" strokeLinecap="round" fill="none" />
</svg>
The ghost sits in a w-40 h-40 container with mb-[-20px] so it overlaps the top edge of the card below it, giving the appearance of hovering directly above the scroll.

Animations

Continuous float (ghost + shadow)

Both the ghost avatar and its elliptical shadow use the same looping animation parameters, linked by testimonial.delay so each ghost floats in its own rhythm:
// Ghost body
animate={{ y: [0, -20, 0], rotate: [-2, 2, -2] }}
transition={{ duration: 5, repeat: Infinity, ease: 'easeInOut', delay: testimonial.delay }}

// Shadow below
animate={{ scale: [1, 0.8, 1], opacity: [0.5, 0.2, 0.5] }}
transition={{ duration: 5, repeat: Infinity, ease: 'easeInOut', delay: testimonial.delay }}
The ghost moves 20px upward and back while rocking ±2°; the shadow simultaneously shrinks and fades, reinforcing the sense of height change.

Card entrance

The outer motion.div wrapper on the Testimonials page slides each card up from y: 50 with a staggered delay:
<motion.div
  initial={{ opacity: 0, y: 50 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ delay: index * 0.3 }}
>
  <FloatingGhost testimonial={testimonial} />
</motion.div>

Usage

Full testimonials grid

import { FloatingGhost } from './components/FloatingGhost';
import { testimonials } from './assets/main';
import { motion } from 'framer-motion';

export default function TestimonialsPage() {
  return (
    <div className="flex flex-col md:flex-row justify-center items-center md:items-start gap-12 md:gap-8">
      {testimonials.map((testimonial, index) => (
        <motion.div
          key={testimonial.name}
          initial={{ opacity: 0, y: 50 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ delay: index * 0.3 }}
        >
          <FloatingGhost testimonial={testimonial} />
        </motion.div>
      ))}
    </div>
  );
}

Single ghost (preview / testing)

<FloatingGhost
  testimonial={{
    name: 'The Witch',
    role: 'Lead Designer, Cauldron Studio',
    quote: 'Their eye for detail is positively bewitching.',
    delay: 0,
  }}
/>

Card Visual Design

The testimonial card uses a scroll or cushion shape achieved with overlapping rounded bars:
  • Bodybg-[#e2d5c4] with a border-2 border-[#c7bcae] outline and shadow-xl.
  • Top cap — an absolutely positioned h-6 bar in bg-[#d7ccc8] with rounded-full creates the rolled-top-of-scroll effect.
  • Bottom cap — the same treatment mirrored at the bottom.
  • Quote — displayed in font-spooky text-xl wrapped in " " curly quote characters.
  • Attribution — name in font-bold text-sm, role in font-code text-xs opacity-70, separated from the quote by a border-t border-night/10.

Customisation

To add or change testimonials, edit the testimonials array in assets/main.js. Each entry only needs the four fields documented above.
The delay value controls both the entrance animation and the floating phase offset. Spread delays across 0, 0.8, and 1.5 seconds (as the defaults do) so the three ghosts never bob in perfect unison — that synchronised bobbing would look mechanical rather than ethereal.
The ghost shape is a bespoke SVG path, not a Lucide icon. If you want to swap it for a different avatar (e.g. a Lucide ghost icon), replace the <svg> block inside the w-40 h-40 container div. Keep the container dimensions the same so the -mb-[20px] overlap with the card below is preserved.

Build docs developers (and LLMs) love