Tawny

Portfolio

Testimonials

Horizontal quote cards from the vcard. Click a card to open the full note.

components/vcard/testimonials
  • Mina Ortiz

    A short quote so the card can show the clamped excerpt and open the detail view when someone wants the rest of the sentence.

  • Jules Park

    A second card sits beside the first and keeps the row from looking empty. The full note stays in the dialog.

  • Sam Adeyemi

    A third card runs past the edge so the horizontal scrollbar has somewhere to go, and the modal can show this whole paragraph.

Source

Copy the files below into your project.

apps/web/components/vcard/testimonials.tsx
'use client'

import { useEffect, useId, useRef, useState } from 'react'
import Image from 'next/image'
import { Quote, X } from 'lucide-react'
import { testimonials, type VcardTestimonial } from '@/lib/vcard/showcase'
import { cn } from '@tawny/ui/lib/utils'

export function VcardTestimonials({ className }: { className?: string }) {
  const [active, setActive] = useState<VcardTestimonial | null>(null)
  const closeRef = useRef<HTMLButtonElement>(null)
  const titleId = useId()

  useEffect(() => {
    if (!active) return
    closeRef.current?.focus()
    const onKey = (event: KeyboardEvent) => {
      if (event.key === 'Escape') setActive(null)
    }
    document.addEventListener('keydown', onKey)
    return () => document.removeEventListener('keydown', onKey)
  }, [active])

  return (
    <div className={cn('vcard-testimonials', className)}>
      <ul className="vcard-quotes">
        {testimonials.map((item) => (
          <li key={item.id}>
            <div
              role="button"
              tabIndex={0}
              className="vcard-quote vcard-gradient-card"
              onClick={() => setActive(item)}
              onKeyDown={(event) => {
                if (event.key === 'Enter' || event.key === ' ') {
                  event.preventDefault()
                  setActive(item)
                }
              }}
            >
              <div className="vcard-quote-avatar">
                <Image
                  src={item.image}
                  alt=""
                  width={56}
                  height={56}
                />
              </div>
              <h3 className="vcard-quote-name">{item.name}</h3>
              <p className="vcard-quote-text">{item.text}</p>
            </div>
          </li>
        ))}
      </ul>

      {active ? (
        <div
          className="vcard-modal"
          role="dialog"
          aria-modal="true"
          aria-labelledby={titleId}
        >
          <button
            type="button"
            className="vcard-modal-overlay"
            aria-label="Close testimonial"
            onClick={() => setActive(null)}
          />
          <div className="vcard-modal-card">
            <button
              ref={closeRef}
              type="button"
              className="vcard-modal-close"
              aria-label="Close"
              onClick={() => setActive(null)}
            >
              <X size={16} aria-hidden="true" />
            </button>
            <div className="vcard-modal-portrait">
              <div className="vcard-modal-avatar">
                <Image src={active.image} alt="" width={72} height={72} />
              </div>
              <Quote
                className="vcard-modal-quote-icon"
                size={28}
                aria-hidden="true"
              />
            </div>
            <div>
              <h3 id={titleId} className="vcard-modal-name">
                {active.name}
              </h3>
              <time className="vcard-modal-date">{active.date}</time>
              <p className="vcard-modal-text">{active.text}</p>
            </div>
          </div>
        </div>
      ) : null}
    </div>
  )
}