Skip to content

Commit 2042ef8

Browse files
committed
web: port ClusterMAX quote carousel — logo strip + inline quote icon
Mirrors the carousel redesign from SemiAnalysisAI/ClusterMAX-internal#201 so the two public sites stay consistent. - Carousel: replace the uppercase org-name pseudo-tab strip with a clickable infinite-marquee logo strip. Active logo lights up + gets bg-accent/60; inactive are grayscale @ 50% opacity. Falls back to uppercase name when a quote has no logo. Auto-rotates every 8s, pauses on hover/focus, respects prefers-reduced-motion. - Layout: split the bottom of the card into an active-author block (left, with logo + brand bar + name/title + external-link icon) and the existing 'See more supporters →' link (right), aligned to the same baseline. - Quote icon: moved out of the IntroSection heading and inline with the quotation paragraph itself, matching the ClusterMAX fix. - Unitalicized the long-form quote on /quotes (Sam's rule: long-form text should never be italic; carousel was already non-italic). - globals.css: add @Keyframes marquee + .animate-marquee helpers.
1 parent 7fbbd12 commit 2042ef8

4 files changed

Lines changed: 147 additions & 52 deletions

File tree

packages/app/src/app/globals.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,33 @@
510510
animation: star-glow 2s ease-in-out infinite;
511511
pointer-events: none;
512512
}
513+
514+
/* Trusted-by logo strip: infinite horizontal marquee.
515+
The strip renders the logo set twice; translating the inner row by -50%
516+
slides the first copy off-screen exactly as the second copy moves into
517+
its place, producing a seamless loop. Each set carries pr-5 so the
518+
trailing gap is baked into the 50% math. */
519+
@keyframes marquee {
520+
from {
521+
transform: translateX(0);
522+
}
523+
to {
524+
transform: translateX(-50%);
525+
}
526+
}
527+
528+
.animate-marquee {
529+
animation: marquee 50s linear infinite;
530+
will-change: transform;
531+
}
532+
533+
.animate-marquee:hover,
534+
.animate-marquee:focus-within {
535+
animation-play-state: paused;
536+
}
537+
538+
@media (prefers-reduced-motion: reduce) {
539+
.animate-marquee {
540+
animation: none;
541+
}
542+
}

packages/app/src/components/intro-section.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { Quote } from 'lucide-react';
2-
31
import { Card } from '@/components/ui/card';
42
import { MinecraftSplash } from '@/components/minecraft/minecraft-splash';
53
import { QuoteCarousel } from '@/components/quote-carousel';
@@ -17,7 +15,6 @@ export function IntroSection() {
1715
<section>
1816
<Card data-testid="intro-section">
1917
<div className="relative flex items-start gap-2 mb-4">
20-
<Quote className="size-5 shrink-0 mt-1 text-brand" />
2118
<h2 className="text-lg font-semibold">
2219
Open Source Continuous Inference Benchmark Trusted by GigaWatt Token Factories
2320
</h2>

packages/app/src/components/quote-carousel.tsx

Lines changed: 116 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import Link from 'next/link';
44
import { useCallback, useEffect, useRef, useState } from 'react';
5+
import { Quote } from 'lucide-react';
56

67
import { track } from '@/lib/analytics';
78
import { ExternalLinkIcon } from '@/components/ui/external-link-icon';
@@ -66,36 +67,42 @@ function buildCompanyQuotes(quotes: CarouselQuote[], order?: string[]): CompanyE
6667
return shuffleArray(entries);
6768
}
6869

69-
function QuoteBlock({ quote }: { quote: CarouselQuote }) {
70+
function QuoteText({ quote }: { quote: CarouselQuote }) {
7071
return (
71-
<blockquote className="w-full">
72-
<p className="text-sm lg:text-base leading-relaxed text-muted-foreground italic">
73-
&ldquo;{highlightBrand(quote.text)}&rdquo;
72+
<blockquote className="m-0 p-0 border-0">
73+
<p className="text-sm lg:text-base leading-relaxed text-muted-foreground">
74+
<Quote className="inline-block mr-2 -mt-1 size-4 text-brand align-middle" aria-hidden="true" />
75+
{highlightBrand(quote.text)}
7476
</p>
75-
<footer className="mt-3 flex items-center gap-3">
76-
<CompanyLogo org={quote.org} logo={quote.logo} />
77-
<div className="h-12 w-0.5 bg-brand" />
78-
<div className="text-sm">
79-
{quote.link ? (
80-
<a
81-
href={quote.link}
82-
target="_blank"
83-
rel="noopener noreferrer"
84-
className="font-semibold text-foreground hover:text-brand transition-colors group"
85-
>
86-
<span className="group-hover:underline">{quote.name}</span>
87-
<ExternalLinkIcon />
88-
</a>
89-
) : (
90-
<span className="font-semibold text-foreground">{quote.name}</span>
91-
)}
92-
<span className="block text-muted-foreground text-xs">{quote.title}</span>
93-
</div>
94-
</footer>
9577
</blockquote>
9678
);
9779
}
9880

81+
function QuoteAuthor({ quote }: { quote: CarouselQuote }) {
82+
return (
83+
<div className="flex items-center gap-3">
84+
<CompanyLogo org={quote.org} logo={quote.logo} />
85+
<div className="h-12 w-0.5 bg-brand" />
86+
<div className="text-sm">
87+
{quote.link ? (
88+
<a
89+
href={quote.link}
90+
target="_blank"
91+
rel="noopener noreferrer"
92+
className="font-semibold text-foreground hover:text-brand transition-colors group"
93+
>
94+
<span className="group-hover:underline">{quote.name}</span>
95+
<ExternalLinkIcon />
96+
</a>
97+
) : (
98+
<span className="font-semibold text-foreground">{quote.name}</span>
99+
)}
100+
<span className="block text-muted-foreground text-xs">{quote.title}</span>
101+
</div>
102+
</div>
103+
);
104+
}
105+
99106
export function QuoteCarousel({
100107
quotes,
101108
overrides = {},
@@ -160,61 +167,122 @@ export function QuoteCarousel({
160167

161168
return (
162169
<div
163-
className="flex flex-col gap-4"
170+
className="flex flex-col gap-5"
164171
onMouseEnter={() => {
165172
hovering.current = true;
166173
}}
167174
onMouseLeave={() => {
168175
hovering.current = false;
169176
}}
170177
>
171-
{/* Org name strip */}
172-
<div className="flex flex-wrap justify-center gap-x-6 md:gap-x-8 gap-y-2 mx-4">
173-
{entries.map((e, i) => (
174-
<button
175-
key={e.org}
176-
type="button"
177-
onClick={() => goTo(i)}
178-
className={`text-xs font-semibold tracking-wide uppercase transition-colors duration-200 ${
179-
i === activeIndex ? 'text-foreground' : 'text-[#808488] hover:text-muted-foreground'
180-
}`}
181-
>
182-
{labels[e.org] ?? e.org}
183-
</button>
184-
))}
178+
{/* Org logo strip — infinite marquee carousel; clickable, active is highlighted.
179+
Each set carries `pr-5` so the trailing gap is baked into the 50%
180+
translate, keeping the loop seamless. */}
181+
<div className="overflow-hidden">
182+
<div className="flex w-max animate-marquee">
183+
{[0, 1].map((copy) => (
184+
<div
185+
key={copy}
186+
className="flex items-center gap-x-5 pr-5 shrink-0"
187+
aria-hidden={copy === 1 ? true : undefined}
188+
>
189+
{entries.map((e, i) => {
190+
const isActive = i === activeIndex;
191+
return (
192+
<button
193+
key={e.org}
194+
type="button"
195+
onClick={() => goTo(i)}
196+
title={labels[e.org] ?? e.org}
197+
aria-label={`Show quote from ${labels[e.org] ?? e.org}`}
198+
tabIndex={copy === 1 ? -1 : undefined}
199+
className={`group flex h-10 shrink-0 items-center justify-center px-2 rounded-md transition-all duration-200 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 ${
200+
isActive
201+
? 'bg-accent/60'
202+
: 'opacity-50 hover:opacity-100'
203+
}`}
204+
>
205+
{e.quote.logo ? (
206+
<img
207+
src={`/logos/${e.quote.logo}`}
208+
alt={labels[e.org] ?? e.org}
209+
className={`h-6 sm:h-7 max-w-[110px] object-contain transition-all duration-200 ${
210+
isActive ? 'grayscale-0 dark:invert' : 'grayscale dark:invert'
211+
}`}
212+
loading="lazy"
213+
/>
214+
) : (
215+
<span
216+
className={`text-xs font-semibold tracking-wide uppercase ${
217+
isActive ? 'text-foreground' : 'text-muted-foreground'
218+
}`}
219+
>
220+
{labels[e.org] ?? e.org}
221+
</span>
222+
)}
223+
</button>
224+
);
225+
})}
226+
</div>
227+
))}
228+
</div>
185229
</div>
186230

187-
{/* All quotes stacked in same grid cell — tallest sets height */}
188-
<div className="grid items-center">
231+
{/* Stacked quote texts — tallest sets the cell height. */}
232+
<div className="grid items-start">
189233
{entries.map((e, i) => {
190234
const isActive = i === activeIndex;
191235
return (
192236
<div
193237
key={e.org}
194238
className={`col-start-1 row-start-1 ${
195239
isActive
196-
? `transition-opacity duration-300 ease-in-out ${fading ? 'opacity-0' : 'opacity-100'}`
240+
? `transition-opacity duration-300 ease-in-out ${
241+
fading ? 'opacity-0' : 'opacity-100'
242+
}`
197243
: 'opacity-0 invisible pointer-events-none'
198244
}`}
199245
aria-hidden={!isActive}
200246
>
201-
<QuoteBlock quote={e.quote} />
247+
<QuoteText quote={e.quote} />
202248
</div>
203249
);
204250
})}
205251
</div>
206252

207-
{moreHref && (
208-
<div className="flex justify-end">
253+
{/* Bottom row: active quote's author (left) and "See more" link (right),
254+
aligned to the same bottom baseline via items-end. */}
255+
<div className="flex items-end justify-between gap-4">
256+
<div className="grid items-end flex-1 min-w-0">
257+
{entries.map((e, i) => {
258+
const isActive = i === activeIndex;
259+
return (
260+
<div
261+
key={e.org}
262+
className={`col-start-1 row-start-1 ${
263+
isActive
264+
? `transition-opacity duration-300 ease-in-out ${
265+
fading ? 'opacity-0' : 'opacity-100'
266+
}`
267+
: 'opacity-0 invisible pointer-events-none'
268+
}`}
269+
aria-hidden={!isActive}
270+
>
271+
<QuoteAuthor quote={e.quote} />
272+
</div>
273+
);
274+
})}
275+
</div>
276+
{moreHref && (
209277
<Link
210278
href={moreHref}
211-
className="text-xs font-bold text-brand hover:underline"
279+
className="text-xs font-bold text-brand hover:underline shrink-0"
212280
onClick={() => track('quote_carousel_see_more_clicked')}
213281
>
214282
See more supporters &rarr;
215283
</Link>
216-
</div>
217-
)}
284+
)}
285+
</div>
218286
</div>
219287
);
220288
}

packages/app/src/components/quotes/quotes-content.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function QuoteCard({
3434
}) {
3535
const content = (
3636
<blockquote className="space-y-4">
37-
<p className="text-base lg:text-lg leading-relaxed text-muted-foreground italic">
37+
<p className="text-base lg:text-lg leading-relaxed text-muted-foreground">
3838
&ldquo;{highlightBrand(text)}&rdquo;
3939
</p>
4040
<footer className="flex items-center gap-3">

0 commit comments

Comments
 (0)