|
1 | 1 | "use client" |
2 | 2 |
|
3 | | -import { useRef } from "react" |
| 3 | +import { useRef, useCallback, useEffect } from "react" |
4 | 4 | import { motion } from "framer-motion" |
5 | | -import Image from "next/image" |
6 | | -import { TestimonialsMobile } from "./testimonials-mobile" |
| 5 | +import useEmblaCarousel from "embla-carousel-react" |
| 6 | +import AutoPlay from "embla-carousel-autoplay" |
| 7 | +import { ChevronLeft, ChevronRight } from "lucide-react" |
7 | 8 |
|
8 | 9 | export interface Testimonial { |
9 | 10 | id: number |
@@ -47,150 +48,145 @@ export const testimonials: Testimonial[] = [ |
47 | 48 |
|
48 | 49 | export function Testimonials() { |
49 | 50 | const containerRef = useRef<HTMLDivElement>(null) |
| 51 | + const [emblaRef, emblaApi] = useEmblaCarousel( |
| 52 | + { |
| 53 | + loop: true, |
| 54 | + align: "center", |
| 55 | + skipSnaps: false, |
| 56 | + containScroll: false, |
| 57 | + }, |
| 58 | + [ |
| 59 | + AutoPlay({ |
| 60 | + playOnInit: true, |
| 61 | + delay: 4000, |
| 62 | + stopOnInteraction: true, |
| 63 | + stopOnMouseEnter: true, |
| 64 | + stopOnFocusIn: true, |
| 65 | + }), |
| 66 | + ], |
| 67 | + ) |
| 68 | + |
| 69 | + const scrollPrev = useCallback(() => { |
| 70 | + if (emblaApi) emblaApi.scrollPrev() |
| 71 | + }, [emblaApi]) |
| 72 | + |
| 73 | + const scrollNext = useCallback(() => { |
| 74 | + if (emblaApi) emblaApi.scrollNext() |
| 75 | + }, [emblaApi]) |
| 76 | + |
| 77 | + // Re-init auto-play on user interaction |
| 78 | + useEffect(() => { |
| 79 | + if (!emblaApi) return |
| 80 | + |
| 81 | + const autoPlay = emblaApi?.plugins()?.autoPlay as |
| 82 | + | { |
| 83 | + isPlaying?: () => boolean |
| 84 | + play?: () => void |
| 85 | + } |
| 86 | + | undefined |
| 87 | + if (!autoPlay) return |
| 88 | + |
| 89 | + const handleInteraction = () => { |
| 90 | + const isPlaying = autoPlay.isPlaying && autoPlay.isPlaying() |
| 91 | + if (!isPlaying) { |
| 92 | + setTimeout(() => { |
| 93 | + if (autoPlay.play) { |
| 94 | + autoPlay.play() |
| 95 | + } |
| 96 | + }, 2000) |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + emblaApi.on("pointerUp", handleInteraction) |
| 101 | + |
| 102 | + return () => { |
| 103 | + emblaApi.off("pointerUp", handleInteraction) |
| 104 | + } |
| 105 | + }, [emblaApi]) |
50 | 106 |
|
51 | 107 | const containerVariants = { |
52 | 108 | hidden: { opacity: 0 }, |
53 | 109 | visible: { |
54 | 110 | opacity: 1, |
55 | | - transition: { |
56 | | - staggerChildren: 0.15, |
57 | | - delayChildren: 0.3, |
58 | | - }, |
59 | | - }, |
60 | | - } |
61 | | - |
62 | | - const itemVariants = { |
63 | | - hidden: { |
64 | | - opacity: 0, |
65 | | - y: 20, |
66 | | - }, |
67 | | - visible: { |
68 | | - opacity: 1, |
69 | | - y: 0, |
70 | 111 | transition: { |
71 | 112 | duration: 0.6, |
72 | 113 | ease: [0.21, 0.45, 0.27, 0.9], |
73 | 114 | }, |
74 | 115 | }, |
75 | 116 | } |
76 | 117 |
|
77 | | - const backgroundVariants = { |
78 | | - hidden: { |
79 | | - opacity: 0, |
80 | | - }, |
81 | | - visible: { |
82 | | - opacity: 1, |
83 | | - transition: { |
84 | | - duration: 1.2, |
85 | | - ease: "easeOut", |
86 | | - }, |
87 | | - }, |
88 | | - } |
89 | | - |
90 | 118 | return ( |
91 | 119 | <section ref={containerRef} className="relative overflow-hidden border-t border-border py-32"> |
92 | | - <motion.div |
93 | | - className="absolute inset-0" |
94 | | - initial="hidden" |
95 | | - whileInView="visible" |
96 | | - viewport={{ once: true }} |
97 | | - variants={backgroundVariants}> |
98 | | - <div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2"> |
99 | | - <div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-blue-500/10 blur-[120px]" /> |
100 | | - </div> |
101 | | - </motion.div> |
| 120 | + <div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2"> |
| 121 | + <div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-violet-500/10 dark:bg-violet-700/30 blur-[120px]" /> |
| 122 | + </div> |
| 123 | + |
102 | 124 | <div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8"> |
103 | | - <div className="mx-auto mb-24 max-w-3xl text-center"> |
104 | | - <motion.div |
105 | | - initial={{ opacity: 0, y: 20 }} |
106 | | - whileInView={{ opacity: 1, y: 0 }} |
107 | | - viewport={{ once: true }} |
108 | | - transition={{ |
109 | | - duration: 0.6, |
110 | | - ease: [0.21, 0.45, 0.27, 0.9], |
111 | | - }}> |
112 | | - <h2 className="text-4xl font-bold tracking-tight sm:text-5xl"> |
113 | | - Empowering developers worldwide. |
114 | | - </h2> |
115 | | - <p className="mt-6 text-lg text-muted-foreground"> |
116 | | - Join thousands of developers who are revolutionizing their workflow with AI-powered |
117 | | - assistance. |
118 | | - </p> |
119 | | - </motion.div> |
| 125 | + <div className="mx-auto mb-8 max-w-5xl text-center"> |
| 126 | + <h2 className="text-4xl font-bold tracking-tight sm:text-5xl"> |
| 127 | + AI-forward developers are using Roo Code |
| 128 | + </h2> |
| 129 | + <p className="mt-6 text-lg text-muted-foreground"> |
| 130 | + Join more than 800k people revolutionizing their workflow worldwide |
| 131 | + </p> |
120 | 132 | </div> |
121 | 133 |
|
122 | | - {/* Mobile Carousel */} |
123 | | - <TestimonialsMobile /> |
124 | | - |
125 | | - {/* Desktop Grid */} |
126 | 134 | <motion.div |
127 | | - className="relative mx-auto hidden max-w-[1200px] md:block" |
| 135 | + className="relative mx-auto max-w-[1400px]" |
128 | 136 | variants={containerVariants} |
129 | 137 | initial="hidden" |
130 | 138 | whileInView="visible" |
131 | 139 | viewport={{ once: true }}> |
132 | | - <div className="relative grid grid-cols-1 gap-12 md:grid-cols-2"> |
133 | | - {testimonials.map((testimonial, index) => ( |
134 | | - <motion.div |
135 | | - key={testimonial.id} |
136 | | - variants={itemVariants} |
137 | | - className={`group relative ${index % 2 === 0 ? "md:translate-y-4" : "md:translate-y-12"}`}> |
138 | | - <div className="absolute -inset-px rounded-2xl bg-gradient-to-r from-blue-500/30 via-cyan-500/30 to-purple-500/30 opacity-0 blur-sm transition-all duration-500 ease-out group-hover:opacity-100 dark:from-blue-400/40 dark:via-cyan-400/40 dark:to-purple-400/40" /> |
139 | | - <div className="relative flex h-full flex-col rounded-2xl border border-border/50 bg-background/30 backdrop-blur-xl transition-all duration-500 ease-out group-hover:scale-[1.02] group-hover:border-border group-hover:bg-background/40 group-hover:shadow-2xl dark:border-border/70 dark:bg-background/40 dark:group-hover:border-border dark:group-hover:bg-background/60 dark:group-hover:shadow-[0_20px_50px_rgba(59,130,246,0.15)]"> |
140 | | - {testimonial.image && ( |
141 | | - <div className="absolute -right-3 -top-3 h-16 w-16 overflow-hidden rounded-xl border border-border/50 bg-background/50 p-1.5 backdrop-blur-xl transition-all duration-500 ease-out group-hover:scale-110 dark:border-border/70 dark:bg-background/60"> |
142 | | - <div className="relative h-full w-full overflow-hidden rounded-lg"> |
143 | | - <Image |
144 | | - src={testimonial.image || "/placeholder_pfp.png"} |
145 | | - alt={testimonial.name} |
146 | | - fill |
147 | | - className="object-cover" |
148 | | - /> |
149 | | - </div> |
150 | | - </div> |
151 | | - )} |
152 | | - |
153 | | - <div className="flex flex-1 flex-col p-8"> |
154 | | - <div className="flex-1"> |
155 | | - <div className="mb-6"> |
156 | | - <svg |
157 | | - className="h-8 w-8 text-blue-500/20 transition-all duration-500 group-hover:text-blue-500/30 dark:text-blue-400/40 dark:group-hover:text-blue-400/60" |
158 | | - fill="currentColor" |
159 | | - viewBox="0 0 32 32"> |
160 | | - <defs> |
161 | | - <filter id="glow"> |
162 | | - <feGaussianBlur stdDeviation="3" result="coloredBlur" /> |
163 | | - <feMerge> |
164 | | - <feMergeNode in="coloredBlur" /> |
165 | | - <feMergeNode in="SourceGraphic" /> |
166 | | - </feMerge> |
167 | | - </filter> |
168 | | - </defs> |
169 | | - <path |
170 | | - d="M9.352 4C4.456 7.456 1 13.12 1 19.36c0 5.088 3.072 8.064 6.624 8.064 3.36 0 5.856-2.688 5.856-5.856 0-3.168-2.208-5.472-5.088-5.472-.576 0-1.344.096-1.536.192.48-3.264 3.552-7.104 6.624-9.024L9.352 4zm16.512 0c-4.8 3.456-8.256 9.12-8.256 15.36 0 5.088 3.072 8.064 6.624 8.064 3.264 0 5.856-2.688 5.856-5.856 0-3.168-2.304-5.472-5.184-5.472-.576 0-1.248.096-1.44.192.48-3.264 3.456-7.104 6.528-9.024L25.864 4z" |
171 | | - className="dark:filter dark:drop-shadow-[0_0_8px_rgba(96,165,250,0.4)]" |
172 | | - /> |
173 | | - </svg> |
174 | | - </div> |
| 140 | + {/* Previous Button */} |
| 141 | + <button |
| 142 | + onClick={scrollPrev} |
| 143 | + className="absolute left-0 top-1/2 z-20 -translate-y-1/2 rounded-full border border-border/50 bg-background/80 p-2 backdrop-blur-xl transition-all duration-300 hover:scale-110 hover:shadow-lg md:left-4 md:p-3 lg:left-8" |
| 144 | + aria-label="Previous testimonial"> |
| 145 | + <ChevronLeft className="h-5 w-5 text-muted-foreground transition-colors hover:text-foreground md:h-6 md:w-6" /> |
| 146 | + </button> |
175 | 147 |
|
176 | | - <p className="relative text-lg leading-relaxed text-muted-foreground transition-colors duration-300 group-hover:text-foreground/80 dark:text-foreground/70 dark:group-hover:text-foreground/90"> |
177 | | - {testimonial.quote} |
178 | | - </p> |
179 | | - </div> |
| 148 | + {/* Next Button */} |
| 149 | + <button |
| 150 | + onClick={scrollNext} |
| 151 | + className="absolute right-0 top-1/2 z-20 -translate-y-1/2 rounded-full border border-border/50 bg-background/80 p-2 backdrop-blur-xl transition-all duration-300 hover:scale-110 hover:shadow-lg md:right-4 md:p-3 lg:right-8" |
| 152 | + aria-label="Next testimonial"> |
| 153 | + <ChevronRight className="h-5 w-5 text-muted-foreground transition-colors hover:text-foreground md:h-6 md:w-6" /> |
| 154 | + </button> |
| 155 | + |
| 156 | + {/* Gradient Overlays */} |
| 157 | + <div className="absolute inset-y-0 left-0 z-10 w-[10%] bg-gradient-to-r from-background to-transparent pointer-events-none md:w-[15%]" /> |
| 158 | + <div className="absolute inset-y-0 right-0 z-10 w-[10%] bg-gradient-to-l from-background to-transparent pointer-events-none md:w-[15%]" /> |
180 | 159 |
|
181 | | - <div className="relative mt-6"> |
182 | | - <div className="mb-4 h-px w-12 bg-gradient-to-r from-blue-500/50 to-transparent transition-all duration-500 group-hover:w-16 group-hover:from-blue-500/70 dark:from-blue-400/70 dark:group-hover:from-blue-400/90" /> |
183 | | - <h3 className="font-medium text-foreground/90 transition-colors duration-300 dark:text-foreground"> |
184 | | - {testimonial.name} |
185 | | - </h3> |
186 | | - <p className="text-sm text-muted-foreground transition-colors duration-300 dark:text-muted-foreground/80"> |
187 | | - {testimonial.role} at {testimonial.company} |
188 | | - </p> |
| 160 | + {/* Embla Carousel Container */} |
| 161 | + <div className="overflow-hidden" ref={emblaRef}> |
| 162 | + <div className="flex"> |
| 163 | + {testimonials.map((testimonial) => ( |
| 164 | + <div |
| 165 | + key={testimonial.id} |
| 166 | + className="relative min-w-0 flex-[0_0_85%] px-2 md:flex-[0_0_70%] md:px-4 lg:flex-[0_0_60%]"> |
| 167 | + <div className="group relative py-10 h-full"> |
| 168 | + <div className="relative flex h-full flex-col rounded-2xl border border-border bg-background transition-all duration-500 ease-out group-hover:scale-[1.02] group-hover:border-border group-hover:bg-background/40 group-hover:shadow-xl dark:border-border/70 dark:bg-background/40 dark:group-hover:border-border dark:group-hover:bg-background/60 dark:group-hover:shadow-[0_20px_50px_rgba(59,130,246,0.15)]"> |
| 169 | + <div className="flex flex-1 flex-col p-6 md:p-8"> |
| 170 | + <div className="flex-1"> |
| 171 | + <p className="relative text-sm leading-relaxed text-muted-foreground transition-colors duration-300 group-hover:text-foreground/80 dark:text-foreground/70 dark:group-hover:text-foreground/90 md:text-lg"> |
| 172 | + {testimonial.quote} |
| 173 | + </p> |
| 174 | + </div> |
| 175 | + |
| 176 | + <div className="relative mt-4 md:mt-6"> |
| 177 | + <h3 className="font-medium text-foreground/90 transition-colors duration-300 dark:text-foreground"> |
| 178 | + {testimonial.name} |
| 179 | + </h3> |
| 180 | + <p className="text-sm text-muted-foreground transition-colors duration-300 dark:text-muted-foreground/80"> |
| 181 | + {testimonial.role} at {testimonial.company} |
| 182 | + </p> |
| 183 | + </div> |
| 184 | + </div> |
189 | 185 | </div> |
190 | 186 | </div> |
191 | 187 | </div> |
192 | | - </motion.div> |
193 | | - ))} |
| 188 | + ))} |
| 189 | + </div> |
194 | 190 | </div> |
195 | 191 | </motion.div> |
196 | 192 | </div> |
|
0 commit comments