feat(ui): redesign testimonial slider to manual snap carousel with equal card heights
This commit is contained in:
@@ -2,17 +2,59 @@
|
|||||||
padding: var(--section-padding) 20px;
|
padding: var(--section-padding) 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.testimonial-scroll-container {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
gap: 24px;
|
||||||
|
padding-bottom: 32px; /* Space for scrollbar and shadow */
|
||||||
|
|
||||||
|
/* Hide scrollbar for cleaner look, or keep it subtle */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
|
}
|
||||||
|
.testimonial-scroll-container::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari */
|
||||||
|
}
|
||||||
|
|
||||||
.testimonial-card {
|
.testimonial-card {
|
||||||
max-width: 800px;
|
/* On mobile, show ~85% width so the next card peeks in */
|
||||||
margin: 0 auto;
|
flex: 0 0 calc(88% - 24px);
|
||||||
padding: 60px 48px;
|
scroll-snap-align: start;
|
||||||
|
|
||||||
|
/* Equal heights trick: cards stretch to the tallest sibling */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.testimonial-card {
|
||||||
|
/* On tablet, show ~60% width */
|
||||||
|
flex: 0 0 calc(60% - 24px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.testimonial-card {
|
||||||
|
/* On desktop, show ~45% width */
|
||||||
|
flex: 0 0 calc(45% - 24px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.testimonial-quote {
|
.testimonial-quote {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
/* Push author block to the bottom of the card */
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote-mark {
|
.quote-mark {
|
||||||
@@ -27,11 +69,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-quote p {
|
.testimonial-quote p {
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
line-height: 1.7;
|
line-height: 1.6;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-author {
|
.testimonial-author {
|
||||||
@@ -39,6 +83,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
margin-top: auto; /* Force to bottom */
|
||||||
}
|
}
|
||||||
|
|
||||||
.author-photo {
|
.author-photo {
|
||||||
@@ -58,27 +103,3 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-dots {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--glass-border);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot.active {
|
|
||||||
background: var(--accent-color);
|
|
||||||
box-shadow: 0 0 8px var(--accent-color);
|
|
||||||
transform: scale(1.3);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,51 +1,31 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React from 'react'
|
||||||
import './TestimonialSlider.css'
|
import './TestimonialSlider.css'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-rotating testimonial slider with quotes.
|
* Manual testimonial slider (carousel).
|
||||||
* Testimonials are passed as props (from content/meta/testimonials.yaml later).
|
* Equal heights, horizontal scroll, no autoplay.
|
||||||
*/
|
*/
|
||||||
export default function TestimonialSlider({ testimonials = [] }) {
|
export default function TestimonialSlider({ testimonials = [] }) {
|
||||||
const [activeIndex, setActiveIndex] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (testimonials.length <= 1) return
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setActiveIndex(prev => (prev + 1) % testimonials.length)
|
|
||||||
}, 6000)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [testimonials.length])
|
|
||||||
|
|
||||||
if (testimonials.length === 0) return null
|
if (testimonials.length === 0) return null
|
||||||
|
|
||||||
const current = testimonials[activeIndex]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="testimonials-section app-container">
|
<section className="testimonials-section app-container">
|
||||||
<div className="testimonial-card glass-panel">
|
<div className="testimonial-scroll-container">
|
||||||
|
{testimonials.map((current, i) => (
|
||||||
|
<div className="testimonial-card glass-panel" key={i}>
|
||||||
<div className="testimonial-quote">
|
<div className="testimonial-quote">
|
||||||
<span className="quote-mark">"</span>
|
<span className="quote-mark">"</span>
|
||||||
<p>{current.quote}</p>
|
<p>{current.quote}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="testimonial-author">
|
<div className="testimonial-author">
|
||||||
{current.photo && <img src={current.photo} alt={current.name} className="author-photo" />}
|
{current.photo && <img src={current.photo} alt={current.name} className="author-photo" loading="lazy" />}
|
||||||
<div>
|
<div>
|
||||||
<strong>{current.name}</strong>
|
<strong>{current.name}</strong>
|
||||||
<span>{current.role}</span>
|
<span>{current.role}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{testimonials.length > 1 && (
|
|
||||||
<div className="testimonial-dots">
|
|
||||||
{testimonials.map((_, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
className={`dot ${i === activeIndex ? 'active' : ''}`}
|
|
||||||
onClick={() => setActiveIndex(i)}
|
|
||||||
aria-label={`Testimonial ${i + 1}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user