From 32041ce2e0b7678a7a1753c1f5c65059b45221f5 Mon Sep 17 00:00:00 2001 From: Rashad Karanouh <11599358+rashad@users.noreply.github.com> Date: Thu, 23 Jul 2026 06:35:47 +0200 Subject: [PATCH] fix(website): restore condensed marketplace list card (#23161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The condensed marketplace list-card design (sans-serif partner name, description excerpt, a single scope line, and a "View profile →" text CTA) was the intended glowup card, shipped by #22471/#22402. During the #23016 resync, the `PartnerCard.tsx` merge conflict was resolved in favor of main's rich card, so the condensed design never actually landed on `main`. This PR restores the condensed design, ported onto main's current data layer and helpers — none of #23016's profile/case-study/matching work is touched. ## What changed - `PartnerCard.tsx` — rewritten to render the condensed layout (sans-serif `PartnerName`, `CardIntro` description excerpt, `CardFoot` with a single `ScopeLine` and a mono `CardCta` "View profile →" text link, no chip rows, no money row, no LinkedIn icon). Reuses main's data layer and helpers directly: `richTextExcerpt`, `resolvePartnerScopeCards`, `PartnerAvatar`, `titleCaseFallback`, and the shared `CardFrame` shell (same entrance-animation/hover idiom already used by `MarketplaceMatchCard`). No new helper files were needed — everything the condensed design requires already exists on `main`. - `PartnerChipRow.tsx` — deleted (zero remaining importers after the port; grep confirmed only self-reference). - `PartnerMoneyRow.tsx` — deleted (zero remaining importers after the port; grep confirmed only self-reference). Kept untouched: `fetch-live-marketplace-partners.ts`, `marketplace-partner.ts`, `get-marketplace-partners.ts`, `marketplace-partners-source.ts`, all `PartnerProfile*`/case-study/matching files, `MarketplaceGrid.tsx` (already has the `repeat(N, minmax(0, 1fr))` grid columns on main — no change needed), and the shared label helpers `PARTNER_SCOPE_LABELS`/`SERVED_GEO_LABELS`/`SPOKEN_LANGUAGE_LABELS` (still used by `FilterBar.tsx`/`PartnerReachFacts.tsx`). Website-only change — no partners-app version bump, no `.po` catalog changes. ## Test plan - [x] `npx nx typecheck twenty-website` — clean - [x] `npx oxlint -c .oxlintrc.json .` (twenty-website) — 0 warnings, 0 errors - [x] `npx oxfmt --check .` (twenty-website) — clean - [x] `npx jest partners-marketplace` — 12 suites / 54 tests passing - [x] Verified locally against the running dev server (`/partners/list`): 16 partner cards render with the condensed layout (sans-serif name, excerpt, single scope line, "View profile →"), no chip rows/money rows/LinkedIn icons, real partner data renders correctly (e.g. 01GROWTH, Inc) Review in cubic --- .../src/partners-marketplace/PartnerCard.tsx | 201 ++++++------------ .../partners-marketplace/PartnerChipRow.tsx | 92 -------- .../partners-marketplace/PartnerMoneyRow.tsx | 72 ------- .../format-usd-compact.test.ts | 31 --- .../format-usd-compact.ts | 15 -- .../partner-chip-style.ts | 26 --- 6 files changed, 70 insertions(+), 367 deletions(-) delete mode 100644 packages/twenty-website/src/partners-marketplace/PartnerChipRow.tsx delete mode 100644 packages/twenty-website/src/partners-marketplace/PartnerMoneyRow.tsx delete mode 100644 packages/twenty-website/src/partners-marketplace/format-usd-compact.test.ts delete mode 100644 packages/twenty-website/src/partners-marketplace/format-usd-compact.ts delete mode 100644 packages/twenty-website/src/partners-marketplace/partner-chip-style.ts diff --git a/packages/twenty-website/src/partners-marketplace/PartnerCard.tsx b/packages/twenty-website/src/partners-marketplace/PartnerCard.tsx index 38fdd31913..cbde7097fa 100644 --- a/packages/twenty-website/src/partners-marketplace/PartnerCard.tsx +++ b/packages/twenty-website/src/partners-marketplace/PartnerCard.tsx @@ -1,12 +1,12 @@ 'use client'; -import { IconBrandLinkedin } from '@tabler/icons-react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { styled } from '@linaria/react'; import { LocalizedLink } from '@/platform/i18n/LocalizedLink'; import { + color, FONT_WEIGHT, fontFamily, fontSize, @@ -14,30 +14,24 @@ import { semanticColor, spacing, } from '@/tokens'; -import { Button, ExternalLink } from '@/ui'; -import { isSafeHttpUrl } from './is-safe-http-url'; import { CardFrame, type PartnerCardIndexStyle } from './MarketplaceCardFrame'; import { type MarketplacePartner } from './marketplace-partner'; import { PartnerAvatar } from './PartnerAvatar'; -import { PartnerChipRow } from './PartnerChipRow'; -import { PartnerMoneyRow } from './PartnerMoneyRow'; -import { PARTNER_SCOPE_LABELS } from './partner-scope-labels'; +import { resolvePartnerScopeCards } from './resolve-partner-scope-cards'; import { richTextExcerpt } from './rich-text-excerpt'; -import { SERVED_GEO_LABELS } from './served-geo-labels'; -import { SPOKEN_LANGUAGE_LABELS } from './spoken-language-labels'; import { titleCaseFallback } from './title-case-fallback'; const CardArticle = styled(CardFrame)` - gap: ${spacing(5)}; - padding: ${spacing(6)}; + gap: ${spacing(3.5)}; + padding: ${spacing(5.5)} ${spacing(5.5)} ${spacing(4.5)}; will-change: transform; `; -const CardHeader = styled.div` +const CardTop = styled.div` align-items: center; display: flex; - gap: ${spacing(4)}; + gap: ${spacing(3.25)}; `; const HeaderText = styled.div` @@ -46,30 +40,22 @@ const HeaderText = styled.div` min-width: 0; & > * + * { - margin-top: ${spacing(1)}; + margin-top: ${spacing(0.75)}; } `; -const NameRow = styled.div` - align-items: center; - display: flex; - gap: ${spacing(2)}; -`; - const PartnerName = styled.h3` color: ${semanticColor.ink}; - font-family: ${fontFamily('serif')}; - font-size: ${fontSize(6)}; - font-weight: ${FONT_WEIGHT.light}; - letter-spacing: -0.02em; - line-height: ${fontSize(7)}; + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(4.25)}; + font-weight: 600; + letter-spacing: -0.01em; + line-height: 1.2; `; // The card's whole-surface link uses the "stretched link" pattern: an inline // on the name with an absolutely-positioned ::after that covers the -// entire
. Other anchors inside the card (LinkedIn icon, View -// profile) sit above this overlay via z-index, so each stays an independent -// click target without illegal nested elements. +// entire
. const NameLink = styled(LocalizedLink)` color: inherit; text-decoration: none; @@ -88,69 +74,62 @@ const NameLink = styled(LocalizedLink)` } `; -const LinkedinIconLink = styled(ExternalLink)` - align-items: center; - color: ${semanticColor.inkMuted}; - display: inline-flex; - position: relative; - transition: color 0.2s ease; - z-index: 1; - - &:hover { - color: ${semanticColor.ink}; - } -`; - const LocationEyebrow = styled.span` color: ${semanticColor.inkMuted}; font-family: ${fontFamily('mono')}; - font-size: ${fontSize(3)}; - font-weight: ${FONT_WEIGHT.medium}; - letter-spacing: 0.08em; - line-height: ${fontSize(4)}; + font-size: ${fontSize(2.625)}; + letter-spacing: 0.1em; + line-height: 1.2; text-transform: uppercase; `; -const Introduction = styled.p` +const CardIntro = styled.p` -webkit-box-orient: vertical; -webkit-line-clamp: 3; color: ${semanticColor.inkMuted}; display: -webkit-box; font-family: ${fontFamily('sans')}; - font-size: ${fontSize(4)}; - line-height: ${fontSize(5.5)}; + font-size: ${fontSize(3.625)}; + line-height: 1.5; + overflow: hidden; + overflow-wrap: anywhere; +`; + +const CardFoot = styled.div` + align-items: center; + border-top: 1px solid ${semanticColor.line}; + display: flex; + gap: ${spacing(3.5)}; + justify-content: space-between; + margin-top: ${spacing(0.5)}; + padding-top: ${spacing(3.5)}; +`; + +const ScopeLine = styled.p` + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + color: ${semanticColor.inkMuted}; + display: -webkit-box; + flex: 1; + font-family: ${fontFamily('sans')}; + font-size: ${fontSize(3.5)}; + font-weight: ${FONT_WEIGHT.medium}; + line-height: 1.4; + min-width: 0; overflow: hidden; `; -const Divider = styled.hr` - background-color: ${semanticColor.line}; - border: 0; - height: 1px; - width: 100%; -`; - -const ChipRows = styled.div` - display: flex; - flex-direction: column; - - & > * + * { - margin-top: ${spacing(3)}; - } -`; - -// The price row pins to the card bottom (margin-top: auto), so the button — -// its plain trailing sibling — sits right under it with the same gap on every -// card, regardless of how many lines the chip rows took. The flexible space -// lands above the price (between the chips and the price), not between the -// price and the button. -const MoneyRowPin = styled.div` - margin-top: auto; -`; - -const CtaWrapper = styled.div` - display: flex; - position: relative; - z-index: 1; +const CardCta = styled.span` + align-items: center; + color: ${color('blue')}; + display: inline-flex; + flex: 0 0 auto; + font-family: ${fontFamily('mono')}; + font-size: ${fontSize(2.75)}; + gap: ${spacing(1.25)}; + letter-spacing: 0.06em; + text-transform: uppercase; + white-space: nowrap; `; type PartnerCardProps = { @@ -162,7 +141,7 @@ export function PartnerCard({ partner, index }: PartnerCardProps) { const { i18n } = useLingui(); const headingId = `partner-card-heading-${partner.slug}`; const style: PartnerCardIndexStyle = { '--partner-card-index': index }; - // Unprefixed; LocalizedLink (the name link) and the Button add the locale. + // Unprefixed; LocalizedLink (the name link) adds the locale. const profileHref = `/partners/profile/${partner.slug}`; const locationLine = [ @@ -172,77 +151,37 @@ export function PartnerCard({ partner, index }: PartnerCardProps) { .filter(Boolean) .join(', '); - const linkedinHref = - partner.links.linkedin !== null && isSafeHttpUrl(partner.links.linkedin) - ? partner.links.linkedin - : null; + const scopeCards = resolvePartnerScopeCards(partner.partnerScope); + const scopeLine = scopeCards.map((scope) => i18n._(scope.label)).join(' · '); const descriptionExcerpt = richTextExcerpt(partner.description); return ( - + - - - {partner.name} - - {linkedinHref !== null && ( - - - )} - + + {partner.name} + {locationLine.length > 0 && ( {locationLine} )} - + - {descriptionExcerpt} + {descriptionExcerpt} -