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)
---
.../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}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {scopeLine.length > 0 && (
+ {scopeLine}
+ )}
+ {i18n._(msg`View profile`)} →
+
);
}
diff --git a/packages/twenty-website/src/partners-marketplace/PartnerChipRow.tsx b/packages/twenty-website/src/partners-marketplace/PartnerChipRow.tsx
deleted file mode 100644
index 7be3a755db..0000000000
--- a/packages/twenty-website/src/partners-marketplace/PartnerChipRow.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-'use client';
-
-import { type MessageDescriptor } from '@lingui/core';
-import { useLingui } from '@lingui/react';
-import { styled } from '@linaria/react';
-
-import {
- fontFamily,
- FONT_WEIGHT,
- fontSize,
- mediaUp,
- semanticColor,
- spacing,
-} from '@/tokens';
-
-import { partnerChipClassName } from './partner-chip-style';
-import { titleCaseFallback } from './title-case-fallback';
-
-const Row = styled.dl`
- align-items: flex-start;
- display: flex;
- flex-direction: column;
- gap: ${spacing(2)};
-
- ${mediaUp('md')} {
- // Align the label to the first chip's baseline rather than the row's
- // center: when chips wrap to multiple lines in a narrow card, centering
- // floats the label to the middle and visually lifts the first chip into
- // the row above. Baseline keeps the label pinned beside the first chip.
- align-items: baseline;
- flex-direction: row;
- gap: ${spacing(4)};
- }
-`;
-
-const RowLabel = styled.dt`
- color: ${semanticColor.inkMuted};
- font-family: ${fontFamily('mono')};
- font-size: ${fontSize(3)};
- font-weight: ${FONT_WEIGHT.medium};
- letter-spacing: 0.08em;
- line-height: ${fontSize(4)};
- text-transform: uppercase;
-
- ${mediaUp('md')} {
- flex-shrink: 0;
- width: 80px;
- }
-`;
-
-const ChipList = styled.ul`
- display: flex;
- flex-wrap: wrap;
- gap: ${spacing(2)};
- list-style: none;
- padding: 0;
-`;
-
-type PartnerChipRowProps = {
- label: MessageDescriptor;
- values: readonly TValue[];
- valueLabels: Record;
-};
-
-export function PartnerChipRow({
- label,
- values,
- valueLabels,
-}: PartnerChipRowProps) {
- const { i18n } = useLingui();
-
- return (
-
- {i18n._(label)}
-
-
- {values.map((value) => {
- const descriptor = valueLabels[value];
- const text = descriptor
- ? i18n._(descriptor)
- : titleCaseFallback(value);
- return (
-
- {text}
-
- );
- })}
-
-
-
- );
-}
diff --git a/packages/twenty-website/src/partners-marketplace/PartnerMoneyRow.tsx b/packages/twenty-website/src/partners-marketplace/PartnerMoneyRow.tsx
deleted file mode 100644
index ca301dd71b..0000000000
--- a/packages/twenty-website/src/partners-marketplace/PartnerMoneyRow.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-'use client';
-
-import { IconBriefcase, IconCurrencyDollar } from '@tabler/icons-react';
-import { msg } from '@lingui/core/macro';
-import { useLingui } from '@lingui/react';
-import { styled } from '@linaria/react';
-
-import {
- color,
- fontFamily,
- FONT_WEIGHT,
- fontSize,
- semanticColor,
- spacing,
-} from '@/tokens';
-
-import { formatUsdCompact } from './format-usd-compact';
-
-const Row = styled.div`
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- gap: ${spacing(2)};
-`;
-
-const Pill = styled.span`
- align-items: center;
- background-color: ${color('white')};
- border: 1px solid ${semanticColor.line};
- border-radius: 999px;
- color: ${color('black-80')};
- display: inline-flex;
- font-family: ${fontFamily('sans')};
- font-size: ${fontSize(3)};
- font-weight: ${FONT_WEIGHT.medium};
- gap: ${spacing(1.5)};
- line-height: 1;
- padding: ${spacing(1.5)} ${spacing(3)};
-`;
-
-type PartnerMoneyRowProps = {
- hourlyRateUsd: number | null;
- projectBudgetMinUsd: number | null;
-};
-
-export function PartnerMoneyRow({
- hourlyRateUsd,
- projectBudgetMinUsd,
-}: PartnerMoneyRowProps) {
- const { i18n } = useLingui();
- const hourly = formatUsdCompact(hourlyRateUsd);
- const minBudget = formatUsdCompact(projectBudgetMinUsd);
-
- if (!hourly && !minBudget) return null;
-
- return (
-
- {hourly && (
-
-
- {i18n._(msg`${hourly}/hr`)}
-
- )}
- {minBudget && (
-
-
- {i18n._(msg`from ${minBudget}`)}
-
- )}
-
- );
-}
diff --git a/packages/twenty-website/src/partners-marketplace/format-usd-compact.test.ts b/packages/twenty-website/src/partners-marketplace/format-usd-compact.test.ts
deleted file mode 100644
index 697bf14180..0000000000
--- a/packages/twenty-website/src/partners-marketplace/format-usd-compact.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { formatUsdCompact } from './format-usd-compact';
-
-describe('formatUsdCompact', () => {
- it('returns null when the amount is null', () => {
- expect(formatUsdCompact(null)).toBeNull();
- });
-
- it('returns null when the amount is NaN', () => {
- expect(formatUsdCompact(Number.NaN)).toBeNull();
- });
-
- it('renders sub-thousand amounts in full', () => {
- expect(formatUsdCompact(140)).toBe('$140');
- });
-
- it('renders zero', () => {
- expect(formatUsdCompact(0)).toBe('$0');
- });
-
- it('compacts thousands', () => {
- expect(formatUsdCompact(8000)).toBe('$8K');
- });
-
- it('compacts thousands to one decimal', () => {
- expect(formatUsdCompact(8500)).toBe('$8.5K');
- });
-
- it('compacts larger budgets', () => {
- expect(formatUsdCompact(25000)).toBe('$25K');
- });
-});
diff --git a/packages/twenty-website/src/partners-marketplace/format-usd-compact.ts b/packages/twenty-website/src/partners-marketplace/format-usd-compact.ts
deleted file mode 100644
index 4b67fb0c62..0000000000
--- a/packages/twenty-website/src/partners-marketplace/format-usd-compact.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-const COMPACT_FORMATTER = new Intl.NumberFormat('en-US', {
- notation: 'compact',
- style: 'currency',
- currency: 'USD',
- minimumFractionDigits: 0,
- maximumFractionDigits: 1,
-});
-
-// Compact USD for the marketplace pricing pills ($140/hr, from $8K). A null
-// amount (the partner left the rate unset) returns null so the caller can omit
-// the pill entirely rather than render "$0".
-export const formatUsdCompact = (usd: number | null): string | null => {
- if (usd === null || Number.isNaN(usd)) return null;
- return COMPACT_FORMATTER.format(usd);
-};
diff --git a/packages/twenty-website/src/partners-marketplace/partner-chip-style.ts b/packages/twenty-website/src/partners-marketplace/partner-chip-style.ts
deleted file mode 100644
index e440649d34..0000000000
--- a/packages/twenty-website/src/partners-marketplace/partner-chip-style.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { css } from '@linaria/core';
-
-import {
- color,
- fontFamily,
- FONT_WEIGHT,
- fontSize,
- radius,
- semanticColor,
- spacing,
-} from '@/tokens';
-
-// The static label chip on partner cards: a bordered, uppercase mono tag for
-// regions, languages, and categories.
-export const partnerChipClassName = css`
- border: 1px solid ${semanticColor.line};
- border-radius: ${radius(4)};
- color: ${color('black-80')};
- font-family: ${fontFamily('mono')};
- font-size: ${fontSize(3)};
- font-weight: ${FONT_WEIGHT.medium};
- letter-spacing: 0.04em;
- line-height: ${fontSize(4)};
- padding: ${spacing(1)} ${spacing(2.5)};
- text-transform: uppercase;
-`;