feat(02-01): add testimonials carousel and wire into homepage

- Create TestimonialsSection with 5 placeholder reviews
- Auto-rotates every 5s, pauses on hover, clearInterval cleanup
- ChevronLeft/ChevronRight arrow controls and dot indicators
- Wire TestimonialsSection into page.tsx between hero and listings
This commit is contained in:
Chandler Copeland
2026-03-19 15:01:07 -06:00
parent c26a0b1b62
commit 6d701b7faa

View File

@@ -0,0 +1,190 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
// PLACEHOLDER TESTIMONIALS — replace with real client reviews before launch
const TESTIMONIALS = [
{
quote:
'Working with Teressa made buying our first home in Salt Lake County feel effortless. She guided us through every step with patience and expertise.',
name: 'Sarah Mitchell',
},
{
quote:
'Teressa sold our Provo home in under a week — above asking price. Her knowledge of the Utah market is exceptional.',
name: 'James & Karen Olsen',
},
{
quote:
'As first-time buyers, we had so many questions. Teressa answered every one and found us the perfect home in our budget.',
name: 'Tyler Reeves',
},
{
quote:
'Relocating from out of state is stressful, but Teressa made our transition to Utah smooth and seamless.',
name: 'Michelle Torres',
},
{
quote:
"Teressa's negotiating skills saved us thousands. We couldn't be happier with our new home in Herriman.",
name: 'David & Pam Christensen',
},
];
export default function TestimonialsSection() {
const [activeIndex, setActiveIndex] = useState(0);
const [paused, setPaused] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
if (!paused) {
intervalRef.current = setInterval(() => {
setActiveIndex((i) => (i + 1) % TESTIMONIALS.length);
}, 5000);
}
// Cleanup — prevents memory leak on re-render
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [paused]);
function prev() {
setActiveIndex(
(i) => (i - 1 + TESTIMONIALS.length) % TESTIMONIALS.length
);
}
function next() {
setActiveIndex((i) => (i + 1) % TESTIMONIALS.length);
}
const current = TESTIMONIALS[activeIndex];
return (
<section
id="about"
className="testimonials-section"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Decorative gold quote mark */}
<div className="testimonials-quote-mark" aria-hidden="true">
&ldquo;
</div>
<blockquote className="testimonials-blockquote">
<p className="testimonials-quote">{current.quote}</p>
<footer className="testimonials-attribution">
&mdash; {current.name}
</footer>
</blockquote>
{/* Arrow controls */}
<div className="testimonials-arrows">
<button
onClick={prev}
aria-label="Previous testimonial"
className="testimonials-arrow-btn"
>
<ChevronLeft size={24} />
</button>
<button
onClick={next}
aria-label="Next testimonial"
className="testimonials-arrow-btn"
>
<ChevronRight size={24} />
</button>
</div>
{/* Dot indicators */}
<div className="testimonials-dots" role="tablist" aria-label="Testimonials">
{TESTIMONIALS.map((_, i) => (
<button
key={i}
onClick={() => setActiveIndex(i)}
role="tab"
aria-selected={i === activeIndex}
aria-label={`Go to testimonial ${i + 1}`}
className="testimonials-dot"
style={{ opacity: i === activeIndex ? 1 : 0.3 }}
/>
))}
</div>
<style>{`
.testimonials-section {
background-color: #FAF9F7;
padding: 4rem 2rem;
text-align: center;
}
.testimonials-quote-mark {
font-size: 6rem;
line-height: 1;
color: #C9A84C;
font-family: Georgia, serif;
margin-bottom: -1rem;
user-select: none;
}
.testimonials-blockquote {
max-width: 700px;
margin: 0 auto 2rem;
padding: 0;
}
.testimonials-quote {
font-size: 1.125rem;
color: #1B2B4B;
line-height: 1.8;
font-style: italic;
margin-bottom: 1rem;
}
.testimonials-attribution {
font-size: 0.95rem;
color: #1B2B4B;
font-weight: 600;
opacity: 0.8;
}
.testimonials-arrows {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 1.25rem;
}
.testimonials-arrow-btn {
background: none;
border: 2px solid #1B2B4B;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #1B2B4B;
transition: background-color 0.2s, color 0.2s;
}
.testimonials-arrow-btn:hover {
background-color: #1B2B4B;
color: #FAF9F7;
}
.testimonials-dots {
display: flex;
justify-content: center;
gap: 0.5rem;
}
.testimonials-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #1B2B4B;
border: none;
cursor: pointer;
padding: 0;
transition: opacity 0.2s;
}
`}</style>
</section>
);
}