22
33import Image from "next/image" ;
44import type { StaticImageData } from "next/image" ;
5+ import { motion , useReducedMotion } from "framer-motion" ;
56import adobeLogo from "../home/img/adobe.svg" ;
67import canvaLogo from "../home/img/canva.svg" ;
78import circlebackLogo from "../home/img/circleback.svg" ;
@@ -21,6 +22,8 @@ import twilioLogo from "../home/img/twilio.svg";
2122import { cn } from "@/lib/utils" ;
2223import { LinkBox } from "@/components/ui/link-box" ;
2324
25+ const MARQUEE_DURATION_SEC = 40 ;
26+
2427type CompanyLogo = {
2528 name : string ;
2629 logo : StaticImageData ;
@@ -107,66 +110,141 @@ const companies: CompanyLogo[] = [
107110const LogoImage = ( {
108111 logo,
109112 name,
113+ compact = false ,
110114} : {
111115 logo : StaticImageData ;
112116 name : string ;
117+ compact ?: boolean ;
113118} ) => {
119+ if ( compact ) {
120+ return (
121+ < div className = "overflow-hidden h-[40px] -mx-5 flex items-center" >
122+ < Image
123+ src = { logo }
124+ alt = { `${ name } logo` }
125+ className = "h-[56px] w-auto scale-125 transition-[filter] duration-200 hover:filter-[grayscale(1)_brightness(0)_contrast(1.15)] group-hover:filter-[grayscale(1)_brightness(0)_contrast(1.15)]"
126+ sizes = "(max-width: 768px) 30vw"
127+ priority = { false }
128+ />
129+ </ div >
130+ ) ;
131+ }
132+
114133 return (
115134 < Image
116135 src = { logo }
117136 alt = { `${ name } logo` }
118- className = "object-cover max-w-full transition-[filter] duration-200 h-[56px] hover:filter-[grayscale(1)_brightness(0)_contrast(1.15)] group-hover:filter-[grayscale(1)_brightness(0)_contrast(1.15)]"
137+ className = "h-[56px] object-cover max-w-full transition-[filter] duration-200 hover:filter-[grayscale(1)_brightness(0)_contrast(1.15)] group-hover:filter-[grayscale(1)_brightness(0)_contrast(1.15)]"
119138 sizes = "(max-width: 768px) 50vw, (max-width: 1200px) 25vw, 20vw"
120139 priority = { false }
121140 />
122141 ) ;
123142} ;
124143
125- interface EnterpriseLogoGridProps {
126- className ?: string ;
127- small ?: boolean ;
128- }
144+ const visibleCompanies = companies . filter ( ( c ) => ! c . hidden ) ;
129145
130- export const EnterpriseLogoGrid = ( {
131- className = "" ,
132- small = false ,
133- } : EnterpriseLogoGridProps ) => {
146+ function LogoMarqueeItems ( { duplicate = false } : { duplicate ?: boolean } ) {
134147 return (
135- < div
136- className = { cn (
137- "grid grid-cols-3 sm:grid-cols-6 auto-rows-fr px-2 py-2" ,
138- small && "sm:grid-cols-3" ,
139- className ,
140- ) }
141- role = "grid"
142- aria-label = "Enterprise customers using Langfuse"
143- >
144- { companies . filter ( ( c ) => ! c . hidden ) . map ( ( company , index ) => {
148+ < >
149+ { visibleCompanies . map ( ( company ) => {
145150 const hasStory = Boolean ( company . customerStoryPath ) ;
146151 return (
147152 < LinkBox
148153 key = { company . name }
149154 href = { company . customerStoryPath }
150155 tooltip = { hasStory ? "Read story" : undefined }
151156 tooltipPlacement = "bottom-center"
152- className = { cn (
153- "-mr-px -mb-px flex items-center justify-center !p-0" ,
154- index > 5 ? "hidden sm:flex" : "flex" ,
155- ) }
156- role = "gridcell"
157+ className = "shrink-0 flex items-center justify-center !p-0"
158+ aria-hidden = { duplicate || undefined }
159+ tabIndex = { duplicate ? - 1 : undefined }
157160 aria-label = {
158161 hasStory
159162 ? `Read ${ company . name } user story`
160163 : `${ company . name } uses Langfuse`
161164 }
162165 >
163- < LogoImage
164- logo = { company . logo }
165- name = { company . name }
166- />
166+ < LogoImage logo = { company . logo } name = { company . name } compact />
167167 </ LinkBox >
168168 ) ;
169169 } ) }
170- </ div >
170+ </ >
171+ ) ;
172+ }
173+
174+ interface EnterpriseLogoGridProps {
175+ className ?: string ;
176+ small ?: boolean ;
177+ }
178+
179+ export const EnterpriseLogoGrid = ( {
180+ className = "" ,
181+ small = false ,
182+ } : EnterpriseLogoGridProps ) => {
183+ const shouldReduceMotion = useReducedMotion ( ) ;
184+
185+ return (
186+ < >
187+ { /* Mobile: scrolling marquee or static scroll fallback */ }
188+ { shouldReduceMotion ? (
189+ < div
190+ className = { cn ( "sm:hidden overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" , className ) }
191+ aria-label = "Enterprise customers using Langfuse"
192+ >
193+ < div className = "flex items-center w-max py-2" >
194+ < LogoMarqueeItems />
195+ </ div >
196+ </ div >
197+ ) : (
198+ < div
199+ className = { cn ( "sm:hidden overflow-hidden w-full mask-[linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]" , className ) }
200+ aria-label = "Enterprise customers using Langfuse"
201+ >
202+ < motion . div
203+ className = "flex items-center w-max py-2"
204+ animate = { { x : [ "0%" , "-50%" ] } }
205+ transition = { {
206+ duration : MARQUEE_DURATION_SEC ,
207+ repeat : Infinity ,
208+ ease : "linear" ,
209+ } }
210+ >
211+ < LogoMarqueeItems />
212+ < LogoMarqueeItems duplicate />
213+ </ motion . div >
214+ </ div >
215+ ) }
216+
217+ { /* Desktop: grid layout */ }
218+ < div
219+ className = { cn (
220+ "hidden sm:grid sm:grid-cols-6 auto-rows-fr px-2 py-2" ,
221+ small && "sm:grid-cols-3" ,
222+ className ,
223+ ) }
224+ role = "grid"
225+ aria-label = "Enterprise customers using Langfuse"
226+ >
227+ { visibleCompanies . map ( ( company ) => {
228+ const hasStory = Boolean ( company . customerStoryPath ) ;
229+ return (
230+ < LinkBox
231+ key = { company . name }
232+ href = { company . customerStoryPath }
233+ tooltip = { hasStory ? "Read story" : undefined }
234+ tooltipPlacement = "bottom-center"
235+ className = "-mr-px -mb-px flex items-center justify-center !p-0"
236+ role = "gridcell"
237+ aria-label = {
238+ hasStory
239+ ? `Read ${ company . name } user story`
240+ : `${ company . name } uses Langfuse`
241+ }
242+ >
243+ < LogoImage logo = { company . logo } name = { company . name } />
244+ </ LinkBox >
245+ ) ;
246+ } ) }
247+ </ div >
248+ </ >
171249 ) ;
172250} ;
0 commit comments