Skip to content

Commit bd6e3af

Browse files
committed
feat(partners): introduce gold/silver/bronze tiers across grid and rail
Replace score-based dynamic sizing in PartnersGrid and PartnersRail with a tiered system. Each active partner is assigned a tier (gold, silver, or bronze) that drives sizing, layout, and accent styling. Score is retained for in-tier ordering. - Add `PartnerTier` type, `partnerTierLabels`, `partnerTierOrder`, and `partnerTierFlares` (gradients, icons, label colors) shared between PartnersGrid and PartnersRail - PartnersGrid: render each tier as its own Card with a gradient L-shape, rounded TL/BR + sharp TR/BL corners, and a centered pill tier label. Different per-tier sizing with clear visual jumps between tiers. Mobile-responsive (gold 1-col, silver 1-col, bronze 2-col on small) - PartnersRail: group by tier with per-tier max widths, gradient top bars, and centered pill tier labels. Desaturate + brightness-90 logos with hover restoration. Hide scrollbars and prevent flex shrink so the rail no longer clips its content - Move `Become a Partner` link into PartnersRail itself so it shows up on blog pages too (previously docs-only via overlay) - Mark Convex, Fireship, Nozzle, Vercel, Speakeasy inactive - Restructure blog index layout so the rail meets the page edge while keeping content padding intact - Fix GamVrec1 to use `w-full max-w-[300px]` (was hardcoded `w-[300px]`, causing horizontal overflow inside the 280px docs rail) - Tune per-partner image scale on Clerk, Netlify, OpenRouter, PowerSync for visual balance within their tier
1 parent e117e0e commit bd6e3af

7 files changed

Lines changed: 356 additions & 152 deletions

File tree

src/components/DocsLayout.tsx

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -599,10 +599,7 @@ export function DocsLayout({
599599

600600
const [isFullWidth, setIsFullWidth] = useLocalStorage('docsFullWidth', false)
601601

602-
const activePartners = partners.filter(
603-
(d) =>
604-
d.status === 'active' && d.name !== 'Nozzle.io' && d.id !== 'fireship',
605-
)
602+
const activePartners = partners.filter((d) => d.status === 'active')
606603

607604
const groupInitialOpenState = React.useMemo(() => {
608605
return menuConfig.reduce<Record<string, boolean>>((acc, group, index) => {
@@ -937,31 +934,14 @@ export function DocsLayout({
937934
</div>
938935
{!isLandingPage && (
939936
<RightRail breakpoint="md" className="md:w-[280px]">
940-
<div className="relative">
941-
<PartnersRail
942-
analyticsPlacement="docs_right_rail"
943-
analyticsProperties={{
944-
framework: currentFramework.framework,
945-
library_id: libraryId,
946-
}}
947-
partners={activePartners}
948-
/>
949-
<a
950-
href="https://docs.google.com/document/d/1Hg2MzY2TU6U3hFEZ3MLe2oEOM3JS4-eByti3kdJU3I8"
951-
target="_blank"
952-
rel="noopener noreferrer"
953-
className="absolute right-3 top-2 font-medium opacity-60 hover:opacity-100 text-xs hover:underline"
954-
onClick={() => {
955-
trackEvent('become_partner_clicked', {
956-
framework: currentFramework.framework,
957-
library_id: libraryId,
958-
placement: 'docs_right_rail',
959-
})
960-
}}
961-
>
962-
Become a Partner
963-
</a>
964-
</div>
937+
<PartnersRail
938+
analyticsPlacement="docs_right_rail"
939+
analyticsProperties={{
940+
framework: currentFramework.framework,
941+
library_id: libraryId,
942+
}}
943+
partners={activePartners}
944+
/>
965945
<div className="hidden md:block border border-gray-500/20 rounded-l-lg overflow-hidden w-full">
966946
<RecentPostsWidget />
967947
</div>

src/components/Gam.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ export function GamVrec1({
5959
return (
6060
<div
6161
{...props}
62-
className={twMerge('w-[300px] flex flex-col gap-3', props.className)}
62+
className={twMerge(
63+
'w-full max-w-[300px] flex flex-col gap-3',
64+
props.className,
65+
)}
6366
>
6467
{promos.map((promo) => (
6568
<Link

src/components/PartnersGrid.tsx

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import * as React from 'react'
2-
import { partners as allPartners, PartnerImage } from '~/utils/partners'
2+
import {
3+
partners as allPartners,
4+
PartnerImage,
5+
partnerTierFlares,
6+
partnerTierLabels,
7+
partnerTierOrder,
8+
type PartnerTier,
9+
} from '~/utils/partners'
310
import { Card } from '~/components/Card'
411
import { trackEvent, useTrackedImpression } from '~/utils/analytics'
512

@@ -11,6 +18,39 @@ type PartnersGridProps = {
1118
partnersList?: PartnerItem[]
1219
}
1320

21+
const tierLayout: Record<
22+
PartnerTier,
23+
{
24+
flexBasis: string
25+
minHeight: string
26+
logoMaxWidth: string
27+
logoMaxHeight: string
28+
padding: string
29+
}
30+
> = {
31+
gold: {
32+
flexBasis: 'basis-full sm:basis-1/2',
33+
minHeight: 'min-h-[220px]',
34+
logoMaxWidth: 'max-w-[400px]',
35+
logoMaxHeight: 'max-h-[120px]',
36+
padding: 'p-12',
37+
},
38+
silver: {
39+
flexBasis: 'basis-full sm:basis-1/2 md:basis-1/3 lg:basis-1/4',
40+
minHeight: 'min-h-[130px]',
41+
logoMaxWidth: 'max-w-[180px]',
42+
logoMaxHeight: 'max-h-[56px]',
43+
padding: 'p-6',
44+
},
45+
bronze: {
46+
flexBasis: 'basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/5 xl:basis-1/6',
47+
minHeight: 'min-h-[100px]',
48+
logoMaxWidth: 'max-w-[110px]',
49+
logoMaxHeight: 'max-h-[36px]',
50+
padding: 'p-4',
51+
},
52+
}
53+
1454
function PartnerGridItem({
1555
analyticsPlacement,
1656
analyticsProperties,
@@ -33,18 +73,18 @@ function PartnerGridItem({
3373
},
3474
})
3575

36-
const width = Math.max(Math.round(120 + 280 * partner.score), 150)
76+
const layout = tierLayout[partner.tier ?? 'bronze']
3777

3878
return (
3979
<a
4080
ref={ref}
4181
href={partner.href}
4282
target="_blank"
4383
rel="noreferrer"
44-
className="flex items-center justify-center p-6
84+
className={`flex items-center justify-center
4585
border-r border-b border-gray-200 dark:border-gray-800
46-
hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors duration-150 ease-out"
47-
style={{ width, flexGrow: partner.score }}
86+
hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors duration-150 ease-out
87+
${layout.flexBasis} ${layout.minHeight} ${layout.padding}`}
4888
onClick={() => {
4989
trackEvent('partner_card_clicked', {
5090
partner_id: partner.id,
@@ -56,7 +96,15 @@ function PartnerGridItem({
5696
})
5797
}}
5898
>
59-
<PartnerImage config={partner.image} alt={partner.name} />
99+
<div
100+
className={`w-full flex items-center justify-center ${layout.logoMaxWidth}`}
101+
>
102+
<PartnerImage
103+
className={`w-full object-contain ${layout.logoMaxHeight}`}
104+
config={partner.image}
105+
alt={partner.name}
106+
/>
107+
</div>
60108
</a>
61109
)
62110
}
@@ -70,22 +118,56 @@ export function PartnersGrid({
70118
(partner) => partner.status === 'active',
71119
)
72120

73-
// Sort by score descending so larger partners come first
74-
const sortedItems = [...items].sort((a, b) => b.score - a.score)
121+
const tiers: Array<PartnerTier> = ['gold', 'silver', 'bronze']
122+
123+
const tiersWithPartners = tiers
124+
.map((tier) => ({
125+
tier,
126+
partners: items
127+
.filter((partner) => (partner.tier ?? 'bronze') === tier)
128+
.sort((a, b) => b.score - a.score),
129+
}))
130+
.filter((row) => row.partners.length > 0)
131+
.sort((a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier])
132+
133+
let slotIndex = 0
75134

76135
return (
77-
<Card className="overflow-hidden">
78-
<div className="flex flex-wrap justify-center items-stretch -mr-px -mb-px">
79-
{sortedItems.map((partner, index) => (
80-
<PartnerGridItem
81-
key={partner.id}
82-
analyticsPlacement={analyticsPlacement}
83-
analyticsProperties={analyticsProperties}
84-
index={index}
85-
partner={partner}
86-
/>
87-
))}
88-
</div>
89-
</Card>
136+
<div className="flex flex-col gap-3">
137+
{tiersWithPartners.map((row) => {
138+
const flare = partnerTierFlares[row.tier]
139+
return (
140+
<Card
141+
key={row.tier}
142+
className={`overflow-hidden rounded-none rounded-l-sm rounded-r-2xl bg-gradient-to-b ${flare.gradientStops}`}
143+
>
144+
<div className="ml-1.5 bg-white dark:bg-gray-900">
145+
<div className="px-4 pt-3 pb-2 border-b border-gray-200 dark:border-gray-800 flex items-center gap-2">
146+
<span className={flare.iconColor}>{flare.icon}</span>
147+
<span
148+
className={`text-[10px] uppercase tracking-[0.18em] font-semibold ${flare.labelColor}`}
149+
>
150+
{partnerTierLabels[row.tier]}
151+
</span>
152+
</div>
153+
<div className="flex flex-wrap items-stretch -mr-px -mb-px">
154+
{row.partners.map((partner) => {
155+
const index = slotIndex++
156+
return (
157+
<PartnerGridItem
158+
key={partner.id}
159+
analyticsPlacement={analyticsPlacement}
160+
analyticsProperties={analyticsProperties}
161+
index={index}
162+
partner={partner}
163+
/>
164+
)
165+
})}
166+
</div>
167+
</div>
168+
</Card>
169+
)
170+
})}
171+
</div>
90172
)
91173
}

0 commit comments

Comments
 (0)