feat(partners): tiered partner display (gold/silver/bronze)#867
feat(partners): tiered partner display (gold/silver/bronze)#867tannerlinsley merged 7 commits intomainfrom
Conversation
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
At the sm breakpoint range (640-767px), both gold and silver were rendering as 2-col, making them visually identical in column count. Skip the 2-col phase for silver (jumps from 1-col to 3-col at sm) and likewise bump bronze from 3-col to 4-col so silver and bronze stay distinct too. Final cascade: - Gold: 1 / 2 / 2 / 2 / 2 (xs / sm / md / lg / xl) - Silver: 1 / 3 / 3 / 4 / 4 - Bronze: 2 / 4 / 4 / 5 / 6
✅ Deploy Preview for tanstack ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughPartner rendering switched from score-based sizing to a tier system (gold/silver/bronze); partners are grouped and sorted by tier/score. Active partner filtering now includes all Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/partners.tsx (1)
62-63: UsepartnerTiersas the single source of truth.This export is now the canonical tier list, but
src/components/PartnersGrid.tsxandsrc/components/RightRail.tsxstill hardcode the same tuple. ImportingpartnerTiersthere avoids silently dropping a future tier.♻️ Suggested follow-up
-const tiers: Array<PartnerTier> = ['gold', 'silver', 'bronze'] +const tiers = partnerTiers🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/partners.tsx` around lines 62 - 63, The components currently hardcode the tier tuple instead of using the canonical exported list; update both PartnersGrid and RightRail to import partnerTiers (and PartnerTier if a type is needed) from the utils module and replace the hardcoded array/tuple with partnerTiers so new tiers are honored automatically; ensure any type annotations that referenced the literal tuple use the exported PartnerTier type or derive from partnerTiers to keep a single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/utils/partners.tsx`:
- Around line 62-63: The components currently hardcode the tier tuple instead of
using the canonical exported list; update both PartnersGrid and RightRail to
import partnerTiers (and PartnerTier if a type is needed) from the utils module
and replace the hardcoded array/tuple with partnerTiers so new tiers are honored
automatically; ensure any type annotations that referenced the literal tuple use
the exported PartnerTier type or derive from partnerTiers to keep a single
source of truth.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bd590352-0e3b-4c9d-9d11-2bc57a03479b
📒 Files selected for processing (7)
src/components/DocsLayout.tsxsrc/components/Gam.tsxsrc/components/PartnersGrid.tsxsrc/components/RightRail.tsxsrc/routes/blog.$.tsxsrc/routes/blog.index.tsxsrc/utils/partners.tsx
Switch from per-cell `group-hover` to a named `group/rail` on the PartnersRail outer container. Hovering anywhere within the rail now restores all logos to full color and brightness in one motion, rather than only the cell directly under the cursor.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/components/RightRail.tsx (1)
103-113: Derive the rendered tier list frompartnerTierOrder.This block hard-codes
['gold', 'silver', 'bronze']and then separately sorts withpartnerTierOrder. That creates drift risk: adding or reordering a tier in~/utils/partnerscan leave this component rendering stale tiers. Prefer deriving the list frompartnerTierOrderdirectly.♻️ Proposed refactor
- const tiers: Array<PartnerTier> = ['gold', 'silver', 'bronze'] + const tiers = (Object.entries(partnerTierOrder) as Array<[PartnerTier, number]>) + .sort((a, b) => a[1] - b[1]) + .map(([tier]) => tier) const rowsByTier = tiers .map((tier) => ({ tier, partners: partners .filter((partner) => (partner.tier ?? 'bronze') === tier) .sort((a, b) => b.score - a.score), })) .filter((row) => row.partners.length > 0) - .sort((a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RightRail.tsx` around lines 103 - 113, The tiers array is hard-coded; replace it by deriving the tier list from partnerTierOrder so the component follows the canonical ordering: compute tiers as Object.keys(partnerTierOrder) cast to Array<PartnerTier> (or equivalent) and then keep the existing rowsByTier logic (filtering partners, sorting by score) but map over that derived tiers list instead of ['gold','silver','bronze']; update references to the tiers variable used when building rowsByTier (and ensure PartnerTier typing remains correct).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/RightRail.tsx`:
- Around line 126-139: The Become a Partner CTA inside the RightRail component
is hidden on small screens because RightRail uses the rail’s responsive classes
(hidden sm:block / hidden md:block); move or duplicate the anchor so it always
renders on phone viewports by either extracting the <a> block (the anchor that
calls trackEvent with analyticsPlacement and analyticsProperties) into a
top-level container that has no hidden sm:block classes or add a small-screen
fallback outside RightRail (e.g., render the same anchor conditionally when
viewport is < sm); ensure the same onClick handler
(trackEvent('become_partner_clicked', { placement: analyticsPlacement,
...analyticsProperties })) and classes are preserved so analytics and styling
remain consistent.
- Around line 225-228: The grayscale-to-color transition is only applied on
hover via the "group-hover/rail:grayscale-0 group-hover/rail:brightness-100"
classes; update the class list used in the JSX (the className passed into
twMerge for the element that includes layout.logoMaxWidth) to also include the
equivalent keyboard focus variant "group-focus-within/rail:grayscale-0
group-focus-within/rail:brightness-100" so that focus via keyboard receives the
same affordance as hover while keeping the existing transition classes intact.
---
Nitpick comments:
In `@src/components/RightRail.tsx`:
- Around line 103-113: The tiers array is hard-coded; replace it by deriving the
tier list from partnerTierOrder so the component follows the canonical ordering:
compute tiers as Object.keys(partnerTierOrder) cast to Array<PartnerTier> (or
equivalent) and then keep the existing rowsByTier logic (filtering partners,
sorting by score) but map over that derived tiers list instead of
['gold','silver','bronze']; update references to the tiers variable used when
building rowsByTier (and ensure PartnerTier typing remains correct).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 377a9006-1cce-4bc7-9b2a-322995bc5e32
📒 Files selected for processing (1)
src/components/RightRail.tsx
| <a | ||
| href="https://docs.google.com/document/d/1Hg2MzY2TU6U3hFEZ3MLe2oEOM3JS4-eByti3kdJU3I8" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="font-medium opacity-60 hover:opacity-100 text-xs hover:underline" | ||
| onClick={() => { | ||
| trackEvent('become_partner_clicked', { | ||
| placement: analyticsPlacement, | ||
| ...analyticsProperties, | ||
| }) | ||
| }} | ||
| > | ||
| Become a Partner | ||
| </a> |
There was a problem hiding this comment.
The new CTA is still unavailable on mobile.
Because this link now lives inside RightRail, it inherits the rail’s hidden sm:block / hidden md:block behavior and disappears below the breakpoint. That undercuts the “site-wide” rollout for phone-sized viewports. Please add a small-screen fallback outside the rail or move the CTA to a container that always renders.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RightRail.tsx` around lines 126 - 139, The Become a Partner
CTA inside the RightRail component is hidden on small screens because RightRail
uses the rail’s responsive classes (hidden sm:block / hidden md:block); move or
duplicate the anchor so it always renders on phone viewports by either
extracting the <a> block (the anchor that calls trackEvent with
analyticsPlacement and analyticsProperties) into a top-level container that has
no hidden sm:block classes or add a small-screen fallback outside RightRail
(e.g., render the same anchor conditionally when viewport is < sm); ensure the
same onClick handler (trackEvent('become_partner_clicked', { placement:
analyticsPlacement, ...analyticsProperties })) and classes are preserved so
analytics and styling remain consistent.
| className={twMerge( | ||
| 'w-full flex items-center justify-center mx-auto grayscale brightness-90 group-hover/rail:grayscale-0 group-hover/rail:brightness-100 transition-[filter] duration-500 ease-out', | ||
| layout.logoMaxWidth, | ||
| )} |
There was a problem hiding this comment.
Mirror the rail-level hover state for keyboard focus.
The new full-color treatment is hover-only right now. When users tab through the links, the logos stay grayscale because there’s no group-focus-within/rail equivalent. Please add a focus path so keyboard navigation gets the same affordance.
⌨️ Proposed fix
<div
className={twMerge(
- 'w-full flex items-center justify-center mx-auto grayscale brightness-90 group-hover/rail:grayscale-0 group-hover/rail:brightness-100 transition-[filter] duration-500 ease-out',
+ 'w-full flex items-center justify-center mx-auto grayscale brightness-90 group-hover/rail:grayscale-0 group-hover/rail:brightness-100 group-focus-within/rail:grayscale-0 group-focus-within/rail:brightness-100 transition-[filter] duration-500 ease-out',
layout.logoMaxWidth,
)}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| className={twMerge( | |
| 'w-full flex items-center justify-center mx-auto grayscale brightness-90 group-hover/rail:grayscale-0 group-hover/rail:brightness-100 transition-[filter] duration-500 ease-out', | |
| layout.logoMaxWidth, | |
| )} | |
| className={twMerge( | |
| 'w-full flex items-center justify-center mx-auto grayscale brightness-90 group-hover/rail:grayscale-0 group-hover/rail:brightness-100 group-focus-within/rail:grayscale-0 group-focus-within/rail:brightness-100 transition-[filter] duration-500 ease-out', | |
| layout.logoMaxWidth, | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RightRail.tsx` around lines 225 - 228, The grayscale-to-color
transition is only applied on hover via the "group-hover/rail:grayscale-0
group-hover/rail:brightness-100" classes; update the class list used in the JSX
(the className passed into twMerge for the element that includes
layout.logoMaxWidth) to also include the equivalent keyboard focus variant
"group-focus-within/rail:grayscale-0 group-focus-within/rail:brightness-100" so
that focus via keyboard receives the same affordance as hover while keeping the
existing transition classes intact.
Active partners on /partners now render as three sections (Gold, Silver, Bronze) with section dividers and a centered tier-color pill header. Each tier uses its own card sizing tailored for the dedicated partners page (Gold = 2-up large cards with full description, Silver = 3-up medium with description, Bronze = 4-up compact, name + tagline). When a library filter or the previous-partners view is active, the flat 3-col grid is preserved since tier grouping doesn't apply. Also: - Drop the Lifetime Support Share section and its NetlifyImage import
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/routes/partners.index.tsx (2)
460-479: Give each tier section an actual heading.These new groups are rendered as semantic
sections, but the label is only a styleddiv. That leaves the sections effectively unnamed for heading/landmark navigation. Making the pill anh2and connecting it witharia-labelledbywould preserve the design and improve accessibility.♿ Suggested adjustment
-function TierSectionHeader({ tier }: { tier: PartnerTier }) { +function TierSectionHeader({ + tier, + headingId, +}: { + tier: PartnerTier + headingId: string +}) { const flare = partnerTierFlares[tier] return ( <div className="flex items-center gap-4 mb-8"> <div className={`h-px flex-1 bg-gradient-to-r from-transparent ${flare.gradientStops}`} /> - <div + <h2 + id={headingId} className={`flex items-center gap-2 px-3 py-1 rounded-full ${flare.labelColor}`} > <span className={flare.iconColor}>{flare.icon}</span> <span className="text-xs uppercase tracking-[0.2em] font-bold"> {partnerTierLabels[tier]} </span> - </div> + </h2> <div className={`h-px flex-1 bg-gradient-to-l from-transparent ${flare.gradientStops}`} /> </div> ) } @@ - <section key={section.tier}> - <TierSectionHeader tier={section.tier} /> + <section + key={section.tier} + aria-labelledby={`partner-tier-${section.tier}`} + > + <TierSectionHeader + tier={section.tier} + headingId={`partner-tier-${section.tier}`} + />Also applies to: 503-524
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/partners.index.tsx` around lines 460 - 479, The TierSectionHeader currently renders the tier label as a styled div, leaving semantic sections unnamed; update TierSectionHeader to render the pill as a real heading element (e.g., an h2) using the existing flare styling (partnerTierFlares, partnerTierLabels, flare.icon, flare.* classes) and give the parent semantic section an aria-labelledby that points to the heading's id so screen readers can identify the section; ensure each generated heading has a unique id (e.g., derived from the tier value) and keep the visual styling identical to preserve design.
310-350: Derive tier literals from the shared partner config.
CardSizeandtiersboth re-state thegold/silver/bronzeset that already exists insrc/utils/partners.tsx. The hardcodedtiersarray is the risky part: if a newPartnerTieris added later, this page will silently stop rendering that tier becauseArray<PartnerTier>is not exhaustive. Prefertype CardSize = PartnerTier | 'flat'and iteratepartnerTiersdirectly.♻️ Suggested cleanup
import { partners, PartnerImage, + partnerTiers, partnerTierFlares, partnerTierLabels, partnerTierOrder, type PartnerTier, } from '~/utils/partners' @@ -type CardSize = 'gold' | 'silver' | 'bronze' | 'flat' +type CardSize = PartnerTier | 'flat' @@ - const tiers: Array<PartnerTier> = ['gold', 'silver', 'bronze'] + const tiers = partnerTiersAlso applies to: 489-499
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/partners.index.tsx` around lines 310 - 350, Replace the hardcoded CardSize union and tier list with the shared PartnerTier definitions: change the CardSize type to "PartnerTier | 'flat'" (referencing PartnerTier) and rebuild cardSizeLayout so its keys are derived from partnerTiers plus the 'flat' entry (ensure 'flat' retains its layout). Update any other duplicated tier arrays (the similar block later around the other card layout) to iterate partnerTiers instead of a hardcoded ['gold','silver','bronze'] so adding a new PartnerTier will automatically be included; keep existing layout values mapped to each PartnerTier name and preserve the special 'flat' mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/routes/partners.index.tsx`:
- Around line 460-479: The TierSectionHeader currently renders the tier label as
a styled div, leaving semantic sections unnamed; update TierSectionHeader to
render the pill as a real heading element (e.g., an h2) using the existing flare
styling (partnerTierFlares, partnerTierLabels, flare.icon, flare.* classes) and
give the parent semantic section an aria-labelledby that points to the heading's
id so screen readers can identify the section; ensure each generated heading has
a unique id (e.g., derived from the tier value) and keep the visual styling
identical to preserve design.
- Around line 310-350: Replace the hardcoded CardSize union and tier list with
the shared PartnerTier definitions: change the CardSize type to "PartnerTier |
'flat'" (referencing PartnerTier) and rebuild cardSizeLayout so its keys are
derived from partnerTiers plus the 'flat' entry (ensure 'flat' retains its
layout). Update any other duplicated tier arrays (the similar block later around
the other card layout) to iterate partnerTiers instead of a hardcoded
['gold','silver','bronze'] so adding a new PartnerTier will automatically be
included; keep existing layout values mapped to each PartnerTier name and
preserve the special 'flat' mapping.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4f14f691-9086-4e2f-9c20-7e70c0ff413f
📒 Files selected for processing (1)
src/routes/partners.index.tsx

Summary
scorefor in-tier orderingGamVrec1width bug that overflowed the docs sidebarFiles
src/utils/partners.tsx— addsPartnerTier,partnerTierLabels/Order/Flares(shared icons + gradients), assigns tiers, drops the standalone application-builder tier map (now derived frompartner.tier)src/components/PartnersGrid.tsx— three-card layout, gradient L-shape with rounded TL/BR + sharp TR/BL, centered pill tier labels, mobile-responsive cascade with no breakpoint where two tiers share column countsrc/components/RightRail.tsx— tier grouping with per-tier max widths and a colored top bar per section, centered pill labels, desaturate + brightness-90 logos with hover restoration, hidden scrollbar,[&>*]:shrink-0so the rail no longer clips its contentsrc/components/DocsLayout.tsx— drops the manual "Become a Partner" overlay (now lives inPartnersRail)src/routes/blog.index.tsx— restructured so the rail extends flush to the right edge and the content panel keeps its own paddingsrc/routes/blog.$.tsx— drops redundant Nozzle/Fireship name filters (covered bystatus)src/components/Gam.tsx—w-full max-w-[300px]so the sidebar promo widget no longer horizontal-overflows the 280px docs railTest plan
partner.tier)Summary by CodeRabbit
New Features
Improvements
Bug Fixes