From 6742cfe861cc0be651c3d13cb8ae2126944c9155 Mon Sep 17 00:00:00 2001 From: Rashad Karanouh <11599358+rashad@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:03:46 +0200 Subject: [PATCH] =?UTF-8?q?Marketplace=20glowup=20=E2=80=94=20live=20partn?= =?UTF-8?q?er=20profiles,=20case=20studies=20&=20matching=20(website)=20(#?= =?UTF-8?q?23016)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilds the partners marketplace on live CRM-backed partner data: real profiles, case studies, matching/scope cards, and a "match me" entry point in the grid. ## What changed - Marketplace grid and partner cards now fetch, rank, and filter live partner data instead of static fixtures - Partner profile pages render live profile data, including services, portfolio/case studies, and clients - Partner scope/matching cards on the profile page, plus a `MarketplaceMatchCard` as the first tile in the marketplace grid, routing into the client-brief flow - Rich CTA rail on partner profiles (calendar link, website, socials) built from live partner links - Markdown rendering (`react-markdown`) for partner descriptions and case study bodies, including proper heading rendering - Minor route/sitemap adjustments to support the live-data pages ## Architecture / notes This branch was 463 commits behind `main` and was resynced via a single merge (not rebase) to avoid re-resolving the same conflicts repeatedly. Several of the branch's earlier commits (client-brief wizard, `MarketplaceBriefPrompt`, `MarketplaceMatchCard`'s base styling, `PricingEngagementBand`) had already landed on `main` independently, in some cases refactored into shared components (`EngagementBand`, `MarketplaceCardFrame`, `createWebhookForwardingRoute`) — those conflicts were resolved by taking `main`'s already-shipped version. `PartnerCard.tsx` had diverged into two different designs (`main` gained chip rows / money row / LinkedIn icon; this branch gained the live case-study/portfolio data model with markdown descriptions and structured partner links); the resolution keeps this branch's data model (`description` as markdown, `links`/`linkUrls`) while adopting `main`'s card layout, adapting field references accordingly. `PartnerProfileCtas.tsx` keeps this branch's richer link-rail implementation since it's the one that matches the live data model already wired into `PartnerProfile.tsx`. This is the website counterpart to app PR #22929 (glowup, v1.3.0), already deployed to prod, and supersedes the closed drafts #22471 and #22402. Lint, format, targeted marketplace/client-brief jest tests, and `nx typecheck twenty-website` all pass after the merge. Review in cubic --- packages/twenty-website/package.json | 1 + .../[locale]/(site)/partners/list/page.tsx | 7 +- .../(site)/partners/profile/[slug]/page.tsx | 17 +- .../partners-marketplace/CaseStudyModal.tsx | 203 +++++++++ .../CaseStudyPlaceholder.tsx | 87 ++++ .../partners-marketplace/CaseStudyVisual.tsx | 59 +++ .../CaseStudyVisualHover.tsx | 15 + .../src/partners-marketplace/FilterBar.tsx | 3 +- .../partners-marketplace/MarketplaceGrid.tsx | 4 +- .../MarketplaceListSkeleton.tsx | 157 +++++++ .../src/partners-marketplace/PartnerCard.tsx | 18 +- .../PartnerCoverageSection.tsx | 123 ++++++ .../partners-marketplace/PartnerFactsList.tsx | 106 ----- .../partners-marketplace/PartnerProfile.tsx | 123 ++---- .../PartnerProfileAbout.tsx | 87 ++++ .../PartnerProfileCtas.tsx | 192 ++++++--- .../PartnerRatesPanel.tsx | 67 ++- .../PartnerReachFacts.tsx | 110 +++++ .../PartnerSelectedWork.tsx | 290 +++++++++++++ .../partners-marketplace/PartnerServices.tsx | 184 ++++++++ .../ProfileSectionTitle.tsx | 14 + .../src/partners-marketplace/RichText.tsx | 40 ++ .../case-study-card-aspect-ratio.ts | 2 + .../case-study-modal-image-height.ts | 2 + .../case-study-modal.styles.ts | 260 +++++++++++ .../case-study-sample-image.ts | 3 + .../collect-partner-link-urls.ts | 23 + .../collect-partner-links.ts | 32 ++ .../completeness-score.ts | 14 + .../data/partner-scope-options.ts | 36 ++ .../dummy-marketplace-partners.ts | 408 ++++++++++++++++++ .../fetch-live-marketplace-partners.test.ts | 17 +- .../fetch-live-marketplace-partners.ts | 44 +- .../fetch-live-partner-profile.test.ts | 88 ++++ .../fetch-live-partner-profile.ts | 91 ++++ .../filter-partners.test.ts | 13 +- .../format-partner-link-label.ts | 14 + .../get-marketplace-partner-by-slug.ts | 18 +- .../get-marketplace-partners.ts | 15 + .../partners-marketplace/is-ghost-partner.ts | 4 + .../src/partners-marketplace/link-url.ts | 5 + .../marketplace-api-types.ts | 5 + .../partners-marketplace/marketplace-copy.ts | 6 + .../marketplace-partner.ts | 28 +- .../marketplace-partners-source.ts | 2 + .../src/partners-marketplace/micros-to-usd.ts | 6 + .../src/partners-marketplace/normalize-url.ts | 7 + .../partner-link-entries.test.ts | 58 +++ .../partner-link-entry.ts | 4 + .../partner-scope-labels.ts | 14 +- .../partner-scope-priority.test.ts | 16 + .../partners-api-fetch.ts | 14 +- .../rank-partners.test.ts | 70 +++ .../src/partners-marketplace/rank-partners.ts | 15 + .../resolve-partner-scope-cards.test.ts | 33 ++ .../resolve-partner-scope-cards.ts | 19 + .../rich-text-excerpt.test.ts | 23 + .../partners-marketplace/rich-text-excerpt.ts | 14 + .../scope-priority-order.ts | 9 + .../src/partners-marketplace/top-scopes.ts | 13 + yarn.lock | 1 + 61 files changed, 3015 insertions(+), 338 deletions(-) create mode 100644 packages/twenty-website/src/partners-marketplace/CaseStudyModal.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/CaseStudyPlaceholder.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/CaseStudyVisual.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/CaseStudyVisualHover.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/MarketplaceListSkeleton.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/PartnerCoverageSection.tsx delete mode 100644 packages/twenty-website/src/partners-marketplace/PartnerFactsList.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/PartnerProfileAbout.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/PartnerReachFacts.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/PartnerSelectedWork.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/PartnerServices.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/ProfileSectionTitle.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/RichText.tsx create mode 100644 packages/twenty-website/src/partners-marketplace/case-study-card-aspect-ratio.ts create mode 100644 packages/twenty-website/src/partners-marketplace/case-study-modal-image-height.ts create mode 100644 packages/twenty-website/src/partners-marketplace/case-study-modal.styles.ts create mode 100644 packages/twenty-website/src/partners-marketplace/case-study-sample-image.ts create mode 100644 packages/twenty-website/src/partners-marketplace/collect-partner-link-urls.ts create mode 100644 packages/twenty-website/src/partners-marketplace/collect-partner-links.ts create mode 100644 packages/twenty-website/src/partners-marketplace/completeness-score.ts create mode 100644 packages/twenty-website/src/partners-marketplace/data/partner-scope-options.ts create mode 100644 packages/twenty-website/src/partners-marketplace/dummy-marketplace-partners.ts create mode 100644 packages/twenty-website/src/partners-marketplace/fetch-live-partner-profile.test.ts create mode 100644 packages/twenty-website/src/partners-marketplace/fetch-live-partner-profile.ts create mode 100644 packages/twenty-website/src/partners-marketplace/format-partner-link-label.ts create mode 100644 packages/twenty-website/src/partners-marketplace/get-marketplace-partners.ts create mode 100644 packages/twenty-website/src/partners-marketplace/is-ghost-partner.ts create mode 100644 packages/twenty-website/src/partners-marketplace/link-url.ts create mode 100644 packages/twenty-website/src/partners-marketplace/marketplace-api-types.ts create mode 100644 packages/twenty-website/src/partners-marketplace/marketplace-copy.ts create mode 100644 packages/twenty-website/src/partners-marketplace/marketplace-partners-source.ts create mode 100644 packages/twenty-website/src/partners-marketplace/micros-to-usd.ts create mode 100644 packages/twenty-website/src/partners-marketplace/normalize-url.ts create mode 100644 packages/twenty-website/src/partners-marketplace/partner-link-entries.test.ts create mode 100644 packages/twenty-website/src/partners-marketplace/partner-link-entry.ts create mode 100644 packages/twenty-website/src/partners-marketplace/partner-scope-priority.test.ts create mode 100644 packages/twenty-website/src/partners-marketplace/rank-partners.test.ts create mode 100644 packages/twenty-website/src/partners-marketplace/rank-partners.ts create mode 100644 packages/twenty-website/src/partners-marketplace/resolve-partner-scope-cards.test.ts create mode 100644 packages/twenty-website/src/partners-marketplace/resolve-partner-scope-cards.ts create mode 100644 packages/twenty-website/src/partners-marketplace/rich-text-excerpt.test.ts create mode 100644 packages/twenty-website/src/partners-marketplace/rich-text-excerpt.ts create mode 100644 packages/twenty-website/src/partners-marketplace/scope-priority-order.ts create mode 100644 packages/twenty-website/src/partners-marketplace/top-scopes.ts diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json index b459a95a20..774c0d3a78 100644 --- a/packages/twenty-website/package.json +++ b/packages/twenty-website/package.json @@ -27,6 +27,7 @@ "next-with-linaria": "^1.3.0", "react": "19.2.3", "react-dom": "19.2.3", + "react-markdown": "^10.1.0", "server-only": "^0.0.1", "stripe": "^20.3.1", "three": "^0.184.0", diff --git a/packages/twenty-website/src/app/[locale]/(site)/partners/list/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/partners/list/page.tsx index 14321ca3ff..589f74666c 100644 --- a/packages/twenty-website/src/app/[locale]/(site)/partners/list/page.tsx +++ b/packages/twenty-website/src/app/[locale]/(site)/partners/list/page.tsx @@ -6,8 +6,9 @@ import { type LocaleRouteParams, } from '@/platform/i18n/get-route-i18n'; import { resolveLocaleParam } from '@/platform/i18n/resolve-locale-param'; -import { fetchLiveMarketplacePartners } from '@/partners-marketplace/fetch-live-marketplace-partners'; +import { getMarketplacePartners } from '@/partners-marketplace/marketplace-partners-source'; import { MarketplaceClient } from '@/partners-marketplace/MarketplaceClient'; +import { MarketplaceListSkeleton } from '@/partners-marketplace/MarketplaceListSkeleton'; import { MarketplaceHeader } from '@/partners-marketplace/MarketplaceHeader'; import { buildBreadcrumbListJsonLd, @@ -28,7 +29,7 @@ export default async function PartnersMarketplacePage({ const [, communityStats, partners] = await Promise.all([ getRouteI18n(params), getCommunityStats(), - fetchLiveMarketplacePartners(), + getMarketplacePartners(), ]); const locale = resolveLocaleParam((await params).locale); @@ -47,7 +48,7 @@ export default async function PartnersMarketplacePage({
- + }>
diff --git a/packages/twenty-website/src/app/[locale]/(site)/partners/profile/[slug]/page.tsx b/packages/twenty-website/src/app/[locale]/(site)/partners/profile/[slug]/page.tsx index cb84dd1eaf..86c5fe9026 100644 --- a/packages/twenty-website/src/app/[locale]/(site)/partners/profile/[slug]/page.tsx +++ b/packages/twenty-website/src/app/[locale]/(site)/partners/profile/[slug]/page.tsx @@ -6,9 +6,12 @@ import { getCommunityStats } from '@/platform/community'; import { getRouteI18n } from '@/platform/i18n/get-route-i18n'; import { getServerI18n } from '@/platform/i18n/get-server-i18n'; import { resolveLocaleParam } from '@/platform/i18n/resolve-locale-param'; -import { fetchLiveMarketplacePartners } from '@/partners-marketplace/fetch-live-marketplace-partners'; -import { getMarketplacePartnerBySlug } from '@/partners-marketplace/get-marketplace-partner-by-slug'; +import { + getMarketplacePartnerBySlug, + getMarketplacePartners, +} from '@/partners-marketplace/marketplace-partners-source'; import { PartnerProfile } from '@/partners-marketplace/PartnerProfile'; +import { richTextExcerpt } from '@/partners-marketplace/rich-text-excerpt'; import { buildBreadcrumbListJsonLd, JsonLd } from '@/platform/seo'; import { Menu } from '@/sections/menu'; @@ -19,16 +22,10 @@ export const dynamic = 'force-dynamic'; export const dynamicParams = true; export async function generateStaticParams(): Promise> { - const partners = await fetchLiveMarketplacePartners(); + const partners = await getMarketplacePartners(); return partners.map((partner) => ({ slug: partner.slug })); } -// Collapse whitespace and cap to a meta-description length. -const truncateDescription = (text: string, max = 160): string => { - const cleaned = text.replace(/\s+/g, ' ').trim(); - return cleaned.length <= max ? cleaned : `${cleaned.slice(0, max - 1)}…`; -}; - export async function generateMetadata({ params, }: { @@ -43,7 +40,7 @@ export async function generateMetadata({ } return { title: i18n._(msg`${partner.name} — Twenty Partner`), - description: truncateDescription(partner.introduction), + description: richTextExcerpt(partner.description, 160), }; } diff --git a/packages/twenty-website/src/partners-marketplace/CaseStudyModal.tsx b/packages/twenty-website/src/partners-marketplace/CaseStudyModal.tsx new file mode 100644 index 0000000000..682007e629 --- /dev/null +++ b/packages/twenty-website/src/partners-marketplace/CaseStudyModal.tsx @@ -0,0 +1,203 @@ +'use client'; + +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { IconX } from '@tabler/icons-react'; +import { useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import { isSafeHttpUrl } from './is-safe-http-url'; +import { type PartnerCaseStudy } from './marketplace-partner'; +import { caseStudyModalStyles as modal } from './case-study-modal.styles'; +import { CaseStudyVisual } from './CaseStudyVisual'; +import { RichText } from './RichText'; + +const FOCUSABLE_SELECTOR = + 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'; + +export function CaseStudyModal({ + cases, + openIndex, + onClose, +}: { + cases: readonly PartnerCaseStudy[]; + openIndex: number | null; + onClose: () => void; +}) { + const { i18n } = useLingui(); + const [index, setIndex] = useState(openIndex ?? 0); + const [mounted, setMounted] = useState(false); + const panelRef = useRef(null); + const bodyRef = useRef(null); + const closeButtonRef = useRef(null); + const previousFocusRef = useRef(null); + const previousOverflowRef = useRef(''); + const onCloseRef = useRef(onClose); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + onCloseRef.current = onClose; + }); + + useEffect(() => { + if (openIndex !== null) { + setIndex(openIndex); + } + }, [openIndex]); + + useEffect(() => { + bodyRef.current?.scrollTo({ top: 0 }); + }, [index, openIndex]); + + useEffect(() => { + if (openIndex === null || cases[openIndex] === undefined) { + return undefined; + } + + previousFocusRef.current = document.activeElement; + previousOverflowRef.current = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + closeButtonRef.current?.focus(); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + onCloseRef.current(); + return; + } + + if (event.key !== 'Tab' || panelRef.current === null) { + return; + } + + const focusableElements = Array.from( + panelRef.current.querySelectorAll(FOCUSABLE_SELECTOR), + ); + + if (focusableElements.length === 0) { + return; + } + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + if (event.shiftKey && document.activeElement === firstElement) { + event.preventDefault(); + lastElement.focus(); + } else if (!event.shiftKey && document.activeElement === lastElement) { + event.preventDefault(); + firstElement.focus(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = previousOverflowRef.current; + + if (previousFocusRef.current instanceof HTMLElement) { + previousFocusRef.current.focus(); + } + }; + }, [openIndex, cases]); + + if (openIndex === null || !mounted) { + return null; + } + + const caseStudy = cases[index]; + + if (caseStudy === undefined) { + return null; + } + + const imageUrl = + caseStudy.imageUrl !== null && isSafeHttpUrl(caseStudy.imageUrl) + ? caseStudy.imageUrl + : null; + const outboundLink = + caseStudy.link !== null && isSafeHttpUrl(caseStudy.link) + ? caseStudy.link + : null; + const isPreviousDisabled = index === 0; + const isNextDisabled = index === cases.length - 1; + + const handlePrevious = () => { + setIndex((currentIndex) => Math.max(0, currentIndex - 1)); + }; + + const handleNext = () => { + setIndex((currentIndex) => Math.min(cases.length - 1, currentIndex + 1)); + }; + + return createPortal( + + , + document.body, + ); +} diff --git a/packages/twenty-website/src/partners-marketplace/CaseStudyPlaceholder.tsx b/packages/twenty-website/src/partners-marketplace/CaseStudyPlaceholder.tsx new file mode 100644 index 0000000000..3ce47198c2 --- /dev/null +++ b/packages/twenty-website/src/partners-marketplace/CaseStudyPlaceholder.tsx @@ -0,0 +1,87 @@ +import { styled } from '@linaria/react'; + +import { + color, + fontFamily, + fontSize, + FONT_WEIGHT, + GRADIENT, + radius, + semanticColor, + spacing, +} from '@/tokens'; + +import { CASE_STUDY_CARD_ASPECT_RATIO } from './case-study-card-aspect-ratio'; + +const PlaceholderFrame = styled.div` + aspect-ratio: ${CASE_STUDY_CARD_ASPECT_RATIO}; + background: + radial-gradient(120% 90% at 12% 0%, ${color('blue')}14, transparent 55%), + ${GRADIENT.heroGlow}; + overflow: hidden; + position: relative; + width: 100%; + + &::before { + background-image: + linear-gradient(${semanticColor.line} 1px, transparent 1px), + linear-gradient(90deg, ${semanticColor.line} 1px, transparent 1px); + background-size: ${spacing(6)} ${spacing(6)}; + content: ''; + inset: 0; + opacity: 0.65; + pointer-events: none; + position: absolute; + } +`; + +const PlaceholderInner = styled.div` + align-items: center; + display: flex; + inset: 0; + justify-content: center; + position: absolute; +`; + +const ClientMark = styled.span` + backdrop-filter: blur(6px); + background: ${color('white-80')}; + border: 1px solid ${semanticColor.line}; + border-radius: ${radius(2)}; + color: ${semanticColor.ink}; + font-family: ${fontFamily('mono')}; + font-size: ${fontSize(3)}; + font-weight: ${FONT_WEIGHT.medium}; + letter-spacing: 0.16em; + line-height: 1; + padding: ${spacing(2.5)} ${spacing(4)}; + text-transform: uppercase; + + [data-scheme='dark'] & { + background: ${color('black-40')}; + } +`; + +function clientInitials(client: string): string { + const words = client.trim().split(/\s+/).filter(Boolean); + + if (words.length === 0) { + return 'CS'; + } + + if (words.length === 1) { + return words[0]!.slice(0, 2).toUpperCase(); + } + + return `${words[0]!.charAt(0)}${words[1]!.charAt(0)}`.toUpperCase(); +} + +export function CaseStudyPlaceholder({ client }: { client: string }) { + return ( + + ); +} diff --git a/packages/twenty-website/src/partners-marketplace/CaseStudyVisual.tsx b/packages/twenty-website/src/partners-marketplace/CaseStudyVisual.tsx new file mode 100644 index 0000000000..fb28993faa --- /dev/null +++ b/packages/twenty-website/src/partners-marketplace/CaseStudyVisual.tsx @@ -0,0 +1,59 @@ +import { styled } from '@linaria/react'; + +import { color, EASING, radius } from '@/tokens'; + +import { CASE_STUDY_CARD_ASPECT_RATIO } from './case-study-card-aspect-ratio'; +import { CASE_STUDY_MODAL_IMAGE_HEIGHT } from './case-study-modal-image-height'; + +const VisualFrame = styled.div` + background-color: ${color('black-5')}; + overflow: hidden; + position: relative; + width: 100%; + + &[data-size='card'] { + aspect-ratio: ${CASE_STUDY_CARD_ASPECT_RATIO}; + } + + &[data-size='modal'] { + border-radius: ${radius(1.5)}; + height: ${CASE_STUDY_MODAL_IMAGE_HEIGHT}; + } + + &[data-size='card']::after { + background: linear-gradient( + 180deg, + transparent 55%, + ${color('black-10')} 100% + ); + content: ''; + inset: 0; + pointer-events: none; + position: absolute; + } + + & img { + display: block; + height: 100%; + object-fit: cover; + transform: scale(1); + transition: transform 0.55s ${EASING.standard}; + width: 100%; + } +`; + +export function CaseStudyVisual({ + alt, + imageUrl, + size = 'card', +}: { + alt?: string; + imageUrl: string; + size?: 'card' | 'modal'; +}) { + return ( + + {alt + + ); +} diff --git a/packages/twenty-website/src/partners-marketplace/CaseStudyVisualHover.tsx b/packages/twenty-website/src/partners-marketplace/CaseStudyVisualHover.tsx new file mode 100644 index 0000000000..cc4abc758d --- /dev/null +++ b/packages/twenty-website/src/partners-marketplace/CaseStudyVisualHover.tsx @@ -0,0 +1,15 @@ +import { styled } from '@linaria/react'; + +export const CaseStudyVisualHover = styled.div` + overflow: hidden; + width: 100%; + + &[data-layout='featured'] { + flex-shrink: 0; + width: 100%; + } + + &:hover [data-size='card'] img { + transform: scale(1.045); + } +`; diff --git a/packages/twenty-website/src/partners-marketplace/FilterBar.tsx b/packages/twenty-website/src/partners-marketplace/FilterBar.tsx index 8cdb662cca..c30d30a802 100644 --- a/packages/twenty-website/src/partners-marketplace/FilterBar.tsx +++ b/packages/twenty-website/src/partners-marketplace/FilterBar.tsx @@ -10,6 +10,7 @@ import { Button } from '@/ui'; import { ActiveFilterPills, type ActivePill } from './ActiveFilterPills'; import { type FilterCriteria } from './filter-criteria'; import { FilterDropdown } from './FilterDropdown'; +import { MARKETPLACE_COPY } from './marketplace-copy'; import { PARTNER_SCOPE_LABELS } from './partner-scope-labels'; import { PARTNER_SCOPES, type PartnerScope } from './partner-scopes'; import { SERVED_GEO_LABELS } from './served-geo-labels'; @@ -109,7 +110,7 @@ export function FilterBar({ selected={criteria.languages} /> ` + height: ${spacing(3.5)}; + width: ${({ $width = '100%' }) => $width}; +`; + +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 ScopeBar = styled(SkeletonBlock)` + flex: 1; + height: ${spacing(3.25)}; + max-width: 68%; +`; + +const CtaBar = styled(SkeletonBlock)` + height: ${spacing(3.25)}; + width: ${spacing(16)}; +`; + +function PartnerCardSkeleton() { + return ( + + ); +} + +export function MarketplaceListSkeleton() { + return ( + + + {Array.from({ length: SKELETON_CARD_COUNT }, (_, index) => ( + + ))} + + + ); +} diff --git a/packages/twenty-website/src/partners-marketplace/PartnerCard.tsx b/packages/twenty-website/src/partners-marketplace/PartnerCard.tsx index 785aa09f56..38fdd31913 100644 --- a/packages/twenty-website/src/partners-marketplace/PartnerCard.tsx +++ b/packages/twenty-website/src/partners-marketplace/PartnerCard.tsx @@ -23,6 +23,7 @@ import { PartnerAvatar } from './PartnerAvatar'; import { PartnerChipRow } from './PartnerChipRow'; import { PartnerMoneyRow } from './PartnerMoneyRow'; import { PARTNER_SCOPE_LABELS } from './partner-scope-labels'; +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'; @@ -171,7 +172,12 @@ export function PartnerCard({ partner, index }: PartnerCardProps) { .filter(Boolean) .join(', '); - const linkedinSafe = isSafeHttpUrl(partner.linkedinUrl); + const linkedinHref = + partner.links.linkedin !== null && isSafeHttpUrl(partner.links.linkedin) + ? partner.links.linkedin + : null; + + const descriptionExcerpt = richTextExcerpt(partner.description); return ( @@ -186,20 +192,22 @@ export function PartnerCard({ partner, index }: PartnerCardProps) { {partner.name} - {linkedinSafe && ( + {linkedinHref !== null && ( )} - {locationLine} + {locationLine.length > 0 && ( + {locationLine} + )} - {partner.introduction} + {descriptionExcerpt}