11<template >
22 <section class =" py-24 px-6 text-white" >
33 <div class =" max-w-7xl mx-auto text-center mb-16" >
4- <h2 class =" text-5xl lg:text-7xl font-bold text-center mb-16 tracking-tighter" >
5- See What’s Possible When You <br class =" hidden sm:inline-flex" />
4+ <h2
5+ class =" text-5xl lg:text-7xl font-bold text-center mb-16 tracking-tighter" >
6+ See What’s Possible When You
7+ <br class =" hidden sm:inline-flex" />
68 <span class =" text-primary-500" >Have the Right Support</span >
79 </h2 >
810 </div >
911
1012 <!-- HORIZONTAL SCROLLER -->
11- <div class =" relative mx-auto" >
12- <div class =" overflow-x-auto snap-x snap-mandatory scrollbar-none -mx-6 px-6" >
13- <div class =" grid grid-flow-col auto-cols-[minmax(16rem,20rem)] gap-8 py-2" >
13+ <div class =" relative mx-auto max-w-7xl" >
14+ <!-- Prev / Next buttons -->
15+ <button
16+ @click =" scrollBy(-1)"
17+ aria-label =" Previous testimonials"
18+ class =" absolute left-4 top-1/2 -translate-y-1/2 z-20 bg-black/50 backdrop-blur-md text-white rounded-full p-4 text-2xl transition-colors duration-150 testimonial-scroll-button focus:outline-none" >
19+ ‹
20+ </button >
21+ <button
22+ @click =" scrollBy(1)"
23+ aria-label =" Next testimonials"
24+ class =" absolute right-4 top-1/2 -translate-y-1/2 z-20 bg-black/50 backdrop-blur-md text-white rounded-full p-4 text-2xl transition-colors duration-150 testimonial-scroll-button focus:outline-none" >
25+ ›
26+ </button >
27+
28+ <div
29+ ref =" scroller"
30+ role =" region"
31+ aria-label =" Testimonials carousel"
32+ tabindex =" 0"
33+ class =" overflow-x-auto snap-x snap-mandatory scrollbar-none -mx-6 px-6 py-2" >
34+ <div class =" flex gap-8 items-start" >
1435 <!-- CARD -->
15- <div v-for =" (t, i) in testimonials" :key =" i" class =" snap-start group" >
36+ <div
37+ v-for =" (t, i) in testimonials"
38+ :key =" i"
39+ class =" snap-start min-w-[14.4rem] w-[18rem] sm:w-[21.6rem] shrink-0 group" >
1640 <div
17- class =" relative w-full aspect-4/3 sm:aspect-9/16 rounded-3xl overflow-hidden shadow-lg group hover:scale-[1.02] transition duration-300"
18- >
41+ class =" relative w-full rounded-3xl overflow-hidden shadow-lg group- hover:scale-[1.02] transition-transform duration-300"
42+ style = " aspect-ratio : 4 / 3 " >
1943 <img
2044 :src =" t.image"
2145 :alt =" t.name"
22- class =" w-full h-full object-cover grayscale-100 group-hover:grayscale-0 group-hover:sepia-0 transition ease-out duration-300 delay-75"
23- />
46+ :data-seed =" t.name || i"
47+ @error =" onImgError"
48+ class =" w-full h-full object-cover grayscale-100 group-hover:grayscale-0 transition ease-out duration-300" />
2449 </div >
2550
2651 <p class =" text-base text-gray-100 mt-4 italic leading-relaxed" >
2752 “{{ t.quote }}”
2853 <br />
2954 <span
30- class =" not-italic font-semibold text-gray-400 group-hover:text-secondary-400"
31- >
55+ class =" not-italic font-semibold text-gray-400 group-hover:text-secondary-400" >
3256 – {{ t.name }}, {{ t.role }}
3357 </span >
3458 </p >
4064</template >
4165
4266<script setup>
43- import { testimonials } from ' ../data/testimonials' ;
67+ import { onMounted , onBeforeUnmount , ref } from " vue" ;
68+ import { testimonials as rawTestimonials } from " ../data/testimonials" ;
69+
70+ const scroller = ref (null );
71+ const testimonials = ref ([]);
72+
73+ function shuffleArray (array ) {
74+ const shuffled = [... array];
75+ for (let i = shuffled .length - 1 ; i > 0 ; i-- ) {
76+ const j = Math .floor (Math .random () * (i + 1 ));
77+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
78+ }
79+ return shuffled;
80+ }
81+
82+ function scrollBy (direction = 1 ) {
83+ const el = scroller .value ;
84+ if (! el) return ;
85+ const amount = Math .round (el .clientWidth * 0.8 );
86+ el .scrollBy ({ left: direction * amount, behavior: " smooth" });
87+ }
88+
89+ let pointerDown = false ;
90+ let startX = 0 ;
91+ let scrollLeft = 0 ;
92+
93+ function onPointerDown (e ) {
94+ const el = scroller .value ;
95+ if (! el) return ;
96+ pointerDown = true ;
97+ el .setPointerCapture (e .pointerId );
98+ startX = e .clientX ;
99+ scrollLeft = el .scrollLeft ;
100+ }
101+
102+ function onPointerMove (e ) {
103+ if (! pointerDown) return ;
104+ const el = scroller .value ;
105+ if (! el) return ;
106+ const dx = e .clientX - startX;
107+ el .scrollLeft = scrollLeft - dx;
108+ }
109+
110+ function onPointerUp (e ) {
111+ const el = scroller .value ;
112+ if (! el) return ;
113+ pointerDown = false ;
114+ try {
115+ el .releasePointerCapture ? .(e .pointerId );
116+ } catch (err) {
117+ // ignore
118+ }
119+ }
120+
121+ function onKeyDown (e ) {
122+ if (e .key === " ArrowLeft" ) {
123+ e .preventDefault ();
124+ scrollBy (- 1 );
125+ } else if (e .key === " ArrowRight" ) {
126+ e .preventDefault ();
127+ scrollBy (1 );
128+ }
129+ }
130+
131+ function onImgError (e ) {
132+ try {
133+ e .target .onerror = null ;
134+ const seed = e .target ? .dataset ? .seed || String (Date .now ());
135+ const style = " adventurer" ; // fun avatar style
136+ const params = new URLSearchParams ({
137+ seed: seed,
138+ backgroundType: " solid" ,
139+ }).toString ();
140+ e .target .src = ` https://api.dicebear.com/6.x/${ style} /svg?${ params} ` ;
141+ } catch (err) {
142+ // fallback no-op
143+ }
144+ }
145+
146+ onMounted (() => {
147+ testimonials .value = shuffleArray (rawTestimonials);
148+ const el = scroller .value ;
149+ if (! el) return ;
150+ el .addEventListener (" pointerdown" , onPointerDown);
151+ el .addEventListener (" pointermove" , onPointerMove);
152+ el .addEventListener (" pointerup" , onPointerUp);
153+ el .addEventListener (" pointercancel" , onPointerUp);
154+ el .addEventListener (" keydown" , onKeyDown);
155+ });
156+
157+ onBeforeUnmount (() => {
158+ const el = scroller .value ;
159+ if (! el) return ;
160+ el .removeEventListener (" pointerdown" , onPointerDown);
161+ el .removeEventListener (" pointermove" , onPointerMove);
162+ el .removeEventListener (" pointerup" , onPointerUp);
163+ el .removeEventListener (" pointercancel" , onPointerUp);
164+ el .removeEventListener (" keydown" , onKeyDown);
165+ });
44166< / script>
45167
46168< style>
@@ -51,4 +173,9 @@ import { testimonials } from '../data/testimonials';
51173 - ms- overflow- style: none;
52174 scrollbar- width: none;
53175}
176+
177+ .testimonial - scroll- button: hover {
178+ background: oklch (0.9412 0.1999 105.66 ) ! important;
179+ color: black ! important;
180+ }
54181< / style>
0 commit comments