Skip to main content

Documentation Index

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

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

The Testimonials page gives social proof the same visual treatment as every other page in the portfolio — it refuses to be a plain wall of grey quote boxes. Three AquaPanel cards float in a responsive grid row, each carrying a DiceBear-generated avatar, a quoted testimonial, and the reviewer’s name and role. Each card bobs gently on an infinite vertical oscillation, as if suspended in the liquid Aero background, turning a static section into something alive and distinctive.

Visual overview

The three cards are evenly spaced in a centred grid. Each card is tall enough to contain a circular avatar at the top (overlapping the card edge), three to four lines of quoted text in the middle, and the reviewer’s name and role at the bottom. The cards share the same AquaPanel glass surface, but each starts its bob animation at a different phase offset so they do not all rise and fall in unison — instead they produce a gentle wave effect across the trio.

Sarah Jenkins

Product Manager. Phase offset of 0 s — the baseline cycle. Avatar seeded with "Sarah Jenkins".

David Chen

Lead Designer. Phase offset of 0.2 s, placing him slightly behind Sarah in the wave.

Emily Rodriguez

Startup Founder. Phase offset of 0.4 s, completing the three-card wave pattern.

Component structure

Each card is an AquaPanel wrapped in a motion.div running an infinite y keyframe animation. The avatar is generated by the DiceBear Avataaars API, seeded with the reviewer’s name.
// Simplified Testimonials page structure
import { AquaPanel } from '../components/AquaPanel';
import { motion }    from 'framer-motion';

const testimonials = [
  {
    name:  'Sarah Jenkins',
    role:  'Product Manager',
    text:  'Alex brought life back to our UI. The subtle animations and glossy touches increased user engagement by 40%. It\'s like 2006 but fast.',
    delay: 0,
  },
  {
    name:  'David Chen',
    role:  'Lead Designer',
    text:  'Finally, a developer who understands that flat design isn\'t the only way. The attention to detail in the drop shadows alone is worth hiring them.',
    delay: 0.2,
  },
  {
    name:  'Emily Rodriguez',
    role:  'Startup Founder',
    text:  'Our users literally emailed us to say how much they loved clicking the new buttons. I didn\'t know buttons could make people happy.',
    delay: 0.4,
  },
];

export default function Testimonials() {
  return (
    <div className="testimonials-grid">
      {testimonials.map((person, i) => (
        <motion.div
          key={person.name}
          animate={{ y: [0, -15, 0] }}
          transition={{
            duration: 4,
            repeat: Infinity,
            ease: 'easeInOut',
            delay: person.delay,
          }}
        >
          <AquaPanel className="testimonial-card">
            {/* Circular avatar — overlapping the top edge */}
            <div className="avatar-ring">
              <img
                src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${person.name}&backgroundColor=b6e3f4`}
                alt={person.name}
                className="avatar"
              />
            </div>

            <blockquote>"{person.text}"</blockquote>

            <footer>
              <strong>{person.name}</strong>
              <span>{person.role}</span>
            </footer>
          </AquaPanel>
        </motion.div>
      ))}
    </div>
  );
}

The testimonials data array

Testimonial content is stored in the testimonials array in the Testimonials route component inside assets/main.js. Locate it by searching for "Sarah Jenkins":
// In assets/main.js — locate the testimonials array (search for "Sarah Jenkins")
const testimonials = [
  {
    name:  'Sarah Jenkins',       // Displayed as card footer heading
    role:  'Product Manager',     // Displayed as card footer subtext (field is named "role")
    text:  'Your testimonial text here.',  // The quoted body text (field is named "text")
    delay: 0,                     // Seconds of phase offset for the bob animation
  },
  {
    name:  'David Chen',
    role:  'Lead Designer',
    text:  'Your testimonial text here.',
    delay: 0.2,
  },
  {
    name:  'Emily Rodriguez',
    role:  'Startup Founder',
    text:  'Your testimonial text here.',
    delay: 0.4,
  },
];
The DiceBear avatar URL uses the reviewer’s name as the seed. Any string works — changing the name also changes the generated avatar illustration. If you want to keep a specific avatar while renaming the person, decouple the seed from the name by adding a separate seed field.
DiceBear avatars are fetched at runtime from api.dicebear.com. If you are deploying to an environment with a strict Content Security Policy, add api.dicebear.com to your img-src directive, or pre-download the SVGs and bundle them as local assets.

Key interactions

InteractionBehaviour
Page mountAll three cards begin bobbing immediately on mount — no scroll trigger needed
Bob animationy keyframe: [0, -15, 0] over 4 seconds, easeInOut, repeat: Infinity
Phase offsetsdelay: 0 / 0.2 / 0.4 staggers the three bobs into a rolling wave, not a synchronised bounce

Tuning the bob animation

To make the cards bob more vigorously, increase the y keyframe range. To slow the rhythm, increase the duration. To widen the stagger, increase the gap between each card’s delay value:
// Faster, higher bob
animate={{ y: [0, -24, 0] }}  // Larger amplitude
transition={{
  duration: 2,                  // Faster cycle
  repeat: Infinity,
  ease: 'easeInOut',
  delay: person.delay,
}}

// Wider stagger between cards
{ delay: 0 },
{ delay: 0.7 },
{ delay: 1.4 },

Build docs developers (and LLMs) love