Files
supabase/apps/www/app/partners/PartnersContent.tsx
51a167d910 feat(www): partners landing page (#47874)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added a redesigned Partners page with partnership options, benefits,
application steps, FAQs, integration resources, and featured partners.
- Added an on-page partner application form with questions tailored to
the selected partnership type.
- Added a confirmation message with next steps and a link to the OAuth
integration guide.
- **Bug Fixes**
- Updated partner application links to open the form on the Partners
page.
  - Added support for checkbox-group fields in forms.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alan Daniel <stylesshjs@gmail.com>
Co-authored-by: Alex Hall <alex.hall@supabase.io>
Co-authored-by: Dion Zeneli <101271736+Dionysos288@users.noreply.github.com>
2026-09-25 15:53:32 +02:00

316 lines
13 KiB
TypeScript

'use client'
import { ArrowRight, Check } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button } from 'ui'
import BackgroundPattern from '../../components/Partners/BackgroundPattern'
import DefaultLayout from '@/components/Layouts/Default'
import SectionContainer from '@/components/Layouts/SectionContainer'
import SectionHeading from '@/components/Layouts/SectionHeading'
import Panel from '@/components/Panel'
import BecomeAPartner from '@/components/Partners/BecomeAPartner'
import ProductHeaderCentered from '@/components/Sections/ProductHeaderCentered'
import pageData, { type PARTNER_TIER } from '@/data/partners'
type FeaturedPartner = { slug: string; title: string; logo: string }
interface Props {
featuredPartners: FeaturedPartner[]
}
const FaqList = ({ items }: { items: { question: string; answer: string }[] }) => {
return (
<Accordion type="multiple" className="text-foreground-light">
{items.map((faq, i) => {
return (
<div className="border-b py-2" key={i}>
<AccordionItem value={`faq--${i.toString()}`} className="border-none">
<AccordionTrigger>
<span className="text-foreground">{faq.question}</span>
</AccordionTrigger>
<AccordionContent>
<div className="prose text-foreground-lighter">
<ReactMarkdown>{faq.answer}</ReactMarkdown>
</div>
</AccordionContent>
</AccordionItem>
</div>
)
})}
</Accordion>
)
}
export default function PartnersContent({ featuredPartners }: Props) {
const { heroSection } = pageData
return (
<DefaultLayout>
{/* Hero */}
<div className="relative bg-alternative border-b">
<BackgroundPattern />
<SectionContainer>
<ProductHeaderCentered
title={heroSection.title}
h1={heroSection.h1}
subheader={heroSection.subheader}
cta={heroSection.cta}
secondaryCta={heroSection.secondaryCta}
/>
</SectionContainer>
</div>
{/* Three reasons to partner */}
<SectionContainer>
<SectionHeading
eyebrow={pageData.reasonsSection.eyebrow}
title={pageData.reasonsSection.title}
description={pageData.reasonsSection.description}
/>
<div className="mt-12 grid gap-6 md:grid-cols-3">
{pageData.reasons.map((reason) => (
<Panel
key={reason.title}
outerClassName="h-full hover:shadow-none!"
innerClassName="flex flex-col gap-3 p-4 lg:p-6"
>
<h3 className="text-foreground text-xl tracking-tight">{reason.title}</h3>
<p className="text-foreground-lighter text-sm text-pretty">{reason.description}</p>
</Panel>
))}
</div>
</SectionContainer>
{/* Ways to partner */}
<div className="bg-alternative border-y">
<SectionContainer>
<div className="mt-10 grid gap-6 md:grid-cols-3 md:grid-rows-[auto_auto] md:gap-y-0">
<SectionHeading eyebrow="Programs" title={pageData.waysToPartner.title} />
{pageData.waysToPartner.tiers.map((tier: PARTNER_TIER) => {
const timeToLaunch = (tier as { timeToLaunch?: string }).timeToLaunch
return (
<Panel
key={tier.title}
outerClassName="hover:shadow-none! md:row-span-2 md:grid md:grid-rows-subgrid"
innerClassName="inset-x-0 h-full md:grid md:grid-rows-subgrid md:row-span-2"
>
<div className="items-start flex flex-col gap-3 p-4 lg:p-6">
<h3 className="text-foreground text-xl tracking-tight">{tier.title}</h3>
<p className="text-foreground-lighter text-sm text-pretty flex-1">
{tier.description}
</p>
{tier.cta && (
<Button
asChild
size="tiny"
variant="primary"
className="mt-4"
iconRight={tier.cta?.icon}
>
<Link href={tier.cta?.link}>{tier.cta?.label}</Link>
</Button>
)}
</div>
<dl className="space-y-4 p-4 lg:p-6 bg-surface-200 border-t">
<div className="space-y-1">
<dt className="text-foreground-lighter font-mono text-xs uppercase tracking-wide">
Best for
</dt>
<dd className="text-foreground-light text-sm text-pretty">{tier.bestFor}</dd>
</div>
{tier.whatYouGet && (
<div className="space-y-1">
<dt className="text-foreground-lighter font-mono text-xs uppercase tracking-wide">
What you get
</dt>
<dd className="text-foreground-light text-sm text-pretty">
{tier.whatYouGet}
</dd>
</div>
)}
{timeToLaunch && (
<div className="space-y-1">
<dt className="text-foreground-lighter font-mono text-xs uppercase tracking-wide">
Time to launch
</dt>
<dd className="text-foreground-light text-sm">{timeToLaunch}</dd>
</div>
)}
</dl>
</Panel>
)
})}
</div>
</SectionContainer>
</div>
{/* What partnership gets you */}
<SectionContainer>
<div className="grid gap-12 md:grid-cols-2 md:gap-16 items-start">
<SectionHeading
eyebrow="Benefits"
title={pageData.benefits.title}
description="Open to companies building real integrations on Postgres."
/>
<ul role="list" className="flex flex-col gap-3">
{pageData.benefits.items.map((item: any) => (
<li key={item} className="flex items-start gap-3">
<span
aria-hidden="true"
className="bg-surface-200 text-foreground-light mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full"
>
<Check size={12} strokeWidth={2.5} className="shrink-0" />
</span>
<span className="text-foreground-lighter text-pretty">{item}</span>
</li>
))}
</ul>
</div>
</SectionContainer>
{/* How to apply */}
<div className="bg-alternative border-y">
<SectionContainer>
<div className="flex flex-col gap-8 md:flex-row md:items-end md:justify-between">
<SectionHeading eyebrow="Get started" title={pageData.howToApply.title} />
<Button asChild size="small">
<Link href={pageData.howToApply.cta.link}>{pageData.howToApply.cta.label}</Link>
</Button>
</div>
<ol role="list" className="mt-12 grid gap-6 md:grid-cols-3">
{pageData.howToApply.steps.map((step, i) => (
<li key={step.title}>
<Panel
outerClassName="h-full hover:shadow-none!"
innerClassName="flex flex-col gap-3 p-4 lg:p-6 h-full"
>
<span className="text-foreground-lighter font-mono text-sm tabular-nums">
{String(i + 1).padStart(2, '0')}
</span>
<h3 className="text-foreground text-xl tracking-tight">{step.title}</h3>
<p className="text-foreground-lighter text-sm text-pretty">{step.description}</p>
</Panel>
</li>
))}
</ol>
</SectionContainer>
</div>
{/* Featured partners */}
{featuredPartners.length > 0 && (
<SectionContainer>
<div className="flex flex-col items-center text-center gap-3">
<span className="text-primary font-mono text-sm uppercase tracking-wide">Partners</span>
<h2 className="text-foreground text-3xl md:text-4xl tracking-tight max-w-[35ch] text-balance">
{pageData.featuredPartners.title}
</h2>
<p className="text-foreground-lighter text-lg max-w-[56ch] text-pretty">
{pageData.featuredPartners.description}
</p>
</div>
<div className="mt-12 flex flex-col gap-6 overflow-hidden mask-[linear-gradient(to_right,transparent_0%,black_6%,black_94%,transparent_100%)]">
{[
{
partners: featuredPartners.slice(0, Math.ceil(featuredPartners.length / 2)),
reverse: false,
},
{
partners: featuredPartners.slice(Math.ceil(featuredPartners.length / 2)),
reverse: true,
},
].map((row, rowIdx) => (
<div
key={rowIdx}
className={`flex gap-10 md:gap-14 will-change-transform animate-marquee motion-reduce:animate-none motion-reduce:will-change-none transform-3d backface-hidden ${
row.reverse ? 'direction-[reverse]' : ''
}`}
>
{[...row.partners, ...row.partners].map((partner, i) => (
<Link
key={`${partner.slug}-${i}`}
href={`/partners/catalog/${partner.slug}`}
title={partner.title}
aria-hidden={i >= row.partners.length ? 'true' : undefined}
tabIndex={i >= row.partners.length ? -1 : undefined}
className="size-9 md:size-10 shrink-0 flex items-center justify-center backface-hidden"
>
<div className="relative size-full rounded-full overflow-hidden bg-muted">
<Image
src={partner.logo}
alt={partner.title}
fill
className="object-contain"
sizes="40px"
/>
</div>
</Link>
))}
</div>
))}
</div>
<div className="mt-8 flex justify-center">
<Button asChild size="small" iconRight={<ArrowRight />}>
<Link href="/partners/catalog">Browse the Partner Catalog</Link>
</Button>
</div>
</SectionContainer>
)}
{/* Ways you can integrate with Supabase */}
<div className="bg-alternative border-y">
<SectionContainer className="grid gap-4 xl:grid-cols-3 xl:gap-12 items-start">
<SectionHeading eyebrow="Integration points" title={pageData.integrationOptions.title} />
<dl className="col-span-2 grid gap-2 lg:grid-cols-2 xl:grid-cols-3">
{pageData.integrationOptions.options.map((option) => (
<Link
key={option.title}
href={option.href}
className="group block"
target="_blank"
rel="noopener noreferrer"
>
<Panel
outerClassName="h-full"
innerClassName="flex items-start gap-4 p-4 h-full"
hasActiveOnHover
>
<div className="bg-surface-200 text-foreground flex size-10 shrink-0 items-center justify-center rounded-md border transition-all group-hover:scale-105">
{option.icon}
</div>
<div className="flex flex-col gap-1.5 text-foreground">
<dt className="text-balance">{option.title}</dt>
<dd className="text-foreground-lighter text-sm text-pretty">
{option.description}
</dd>
<span className="text-foreground-light text-sm inline-flex items-center gap-1 mt-1">
Read the docs
<ArrowRight
size={14}
className="shrink-0 transition-transform group-hover:translate-x-0.5"
/>
</span>
</div>
</Panel>
</Link>
))}
</dl>
</SectionContainer>
</div>
{/* FAQ */}
<SectionContainer>
<SectionHeading title={pageData.faq.title} align="center" />
<div className="mx-auto max-w-2xl mt-12">
<FaqList items={pageData.faq.items} />
</div>
</SectionContainer>
{/* Inline partner intake form */}
<BecomeAPartner />
</DefaultLayout>
)
}