Files
supabase/apps/www/components/Solutions/PricingComparisonSection.tsx
Prashant SridharanandTerry Sutton 99f0518137 Added a Hosted Postgres page (#46674)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Created a new solution page for Hosted Postgres.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Hosted Postgres solution with a dedicated landing page (hero,
features, pricing comparison, security, results, CTAs)
  * Introduced a reusable pricing comparison table component
* Enhanced feature grids to support optional headers and richer icon
options

* **Updates**
* Updated company stats (databases managed → 44M, daily launches → 200K,
registered developers → 9M)
  * Added Hosted Postgres link to Solutions navigation

* **Refactor**
* Consolidated and exported component prop interfaces for cleaner prop
contracts
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Terry Sutton <saltcod@gmail.com>
2026-06-09 00:40:18 +01:00

122 lines
3.7 KiB
TypeScript

import { Check, Minus } from 'lucide-react'
import React, { type FC, type ReactNode } from 'react'
import { cn } from 'ui'
import { TextLink } from 'ui-patterns/TextLink'
import SectionContainer from '@/components/Layouts/SectionContainer'
export interface PricingComparisonPlan {
name: string
/** Highlights the column visually (eg. the recommended plan). */
highlight?: boolean
}
export interface PricingComparisonRow {
feature: ReactNode
/** One value per plan, in the same order as `plans`. Booleans render as a check or dash. */
values: (ReactNode | boolean)[]
}
export interface PricingComparisonSectionProps {
id?: string
heading: ReactNode
subheading?: ReactNode
plans: PricingComparisonPlan[]
rows: PricingComparisonRow[]
cta?: {
label: string
url: string
}
className?: string
}
const PlanValue = ({ value }: { value: ReactNode | boolean }) => {
if (value === true) {
return <Check className="w-4 h-4 text-brand" strokeWidth={2} aria-label="Included" />
}
if (value === false) {
return (
<Minus className="w-4 h-4 text-foreground-muted" strokeWidth={2} aria-label="Not included" />
)
}
return <span className="text-sm text-foreground">{value}</span>
}
const PricingComparisonSection: FC<PricingComparisonSectionProps> = ({
id,
heading,
subheading,
plans,
rows,
cta,
className,
}) => {
return (
<SectionContainer id={id} className={cn('flex flex-col gap-8 py-16 md:py-24', className)}>
<div className="flex flex-col gap-2 max-w-xl">
<h2 className="h2 text-foreground-lighter m-0!">{heading}</h2>
{subheading && <p className="text-foreground-lighter">{subheading}</p>}
</div>
<div className="w-full overflow-x-auto border border-default rounded-lg bg-surface-75">
<table className="w-full min-w-[480px] border-collapse text-left">
<thead>
<tr className="border-b border-default">
<th
scope="col"
className="py-4 px-4 md:px-6 text-sm font-normal text-foreground-light"
>
What you get
</th>
{plans.map((plan) => (
<th
key={plan.name}
scope="col"
className={cn(
'py-4 px-4 md:px-6 text-sm font-medium text-center',
plan.highlight ? 'text-foreground bg-surface-100' : 'text-foreground'
)}
>
{plan.name}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rowIndex) => (
<tr
key={rowIndex}
className="border-b border-default last:border-b-0 hover:bg-surface-100/50 transition-colors"
>
<th
scope="row"
className="py-3 px-4 md:px-6 text-sm font-normal text-foreground-light"
>
{row.feature}
</th>
{row.values.map((value, valueIndex) => (
<td
key={valueIndex}
className={cn(
'py-3 px-4 md:px-6 text-center',
plans[valueIndex]?.highlight && 'bg-surface-100'
)}
>
<span className="flex items-center justify-center">
<PlanValue value={value} />
</span>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{cta && <TextLink hasChevron label={cta.label} url={cta.url} className="mt-2" />}
</SectionContainer>
)
}
export default PricingComparisonSection