d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
import { Check, ChipLink, ChipTag, cn } from '@sim/emcn'
|
||
import { SlackIcon } from '@/components/icons'
|
||
import type { CellValue } from '@/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data'
|
||
|
||
/** Maps a cell-icon identifier to its brand icon component. */
|
||
const CELL_ICONS = { slack: SlackIcon } as const
|
||
|
||
/** Resolved CTA for a pricing card - label, chip variant, and destination href. */
|
||
export interface PricingCardCta {
|
||
label: string
|
||
variant: 'primary' | 'border-shadow'
|
||
href: string
|
||
}
|
||
|
||
/** A labelled group of feature rows for one plan, transposed from the comparison data. */
|
||
export interface PricingCardSection {
|
||
/** Stable key for the section, used for React reconciliation. */
|
||
key: string
|
||
/** Section header, e.g. `"Rate limits (runs/min)"`. Omit to render the rows without a header. */
|
||
title?: string
|
||
/** Feature rows - this plan's value for each comparison row in the section. */
|
||
rows: { label: string; value: CellValue }[]
|
||
}
|
||
|
||
/**
|
||
* Props for {@link PricingCard}.
|
||
*/
|
||
export interface PricingCardProps {
|
||
/** Plan name, e.g. `"Pro"`. */
|
||
name: string
|
||
/** Headline price, e.g. `"$25"`, `"$0"`, or `"Custom"`. */
|
||
price: string
|
||
/** Small line under the price, e.g. `"per user/month, billed monthly"`. */
|
||
priceSubtext?: string
|
||
/** Optional discount pill next to the price, e.g. `"15% off"`. */
|
||
discountLabel?: string
|
||
/** Resolved CTA - label, variant, and destination href. */
|
||
cta: PricingCardCta
|
||
/** Full feature breakdown for this plan, grouped by section. */
|
||
sections: PricingCardSection[]
|
||
/** Extra outer classes. */
|
||
className?: string
|
||
}
|
||
|
||
/**
|
||
* Renders one plan's value for a feature row: `true` → check, `false` → em-dash,
|
||
* an icon reference → its brand icon, a string → right-aligned text.
|
||
*/
|
||
function FeatureValue({ value }: { value: CellValue }) {
|
||
if (value === true) {
|
||
return <Check className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
|
||
}
|
||
if (value === false) {
|
||
return <span className='select-none text-[var(--text-muted)]'>–</span>
|
||
}
|
||
if (typeof value === 'object') {
|
||
const Icon = CELL_ICONS[value.icon]
|
||
return <Icon className='size-[14px] flex-shrink-0' />
|
||
}
|
||
return (
|
||
<span className='whitespace-nowrap text-right text-[var(--text-primary)] text-sm tabular-nums'>
|
||
{value}
|
||
</span>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Public pricing card - one plan rendered as a self-contained spec sheet: the
|
||
* header (name, price, CTA, segment) above the full comparison breakdown, grouped
|
||
* by section with a hairline rule under each section header.
|
||
*
|
||
* The radius (`rounded-lg`) matches the landing hero's visual panel so the cards
|
||
* line up with the rest of the landing chrome. Every card carries the identical
|
||
* row structure (only values differ) so a grid of them renders at exactly the
|
||
* same height. Feature rows are transposed from the shared comparison data, so
|
||
* the card can never drift from the platform.
|
||
*/
|
||
export function PricingCard({
|
||
name,
|
||
price,
|
||
priceSubtext,
|
||
discountLabel,
|
||
cta,
|
||
sections,
|
||
className,
|
||
}: PricingCardProps) {
|
||
return (
|
||
<article
|
||
className={cn(
|
||
'flex h-full flex-col gap-[22px] rounded-lg border border-[var(--border-1)] bg-[var(--surface-2)] p-5',
|
||
className
|
||
)}
|
||
>
|
||
<div className='flex flex-col gap-[22px]'>
|
||
<h2 className='text-[24px] text-[var(--text-primary)]'>{name}</h2>
|
||
|
||
<div className='flex flex-col'>
|
||
<div className='flex items-center gap-2'>
|
||
<span className='text-[20px] text-[var(--text-primary)] tabular-nums'>{price}</span>
|
||
{discountLabel && <ChipTag variant='mono'>{discountLabel}</ChipTag>}
|
||
</div>
|
||
<p className='text-[var(--text-muted)] text-base'>{priceSubtext ?? ' '}</p>
|
||
</div>
|
||
|
||
<ChipLink
|
||
href={cta.href}
|
||
variant={cta.variant}
|
||
fullWidth
|
||
flush
|
||
className='w-full justify-center'
|
||
>
|
||
{cta.label}
|
||
</ChipLink>
|
||
</div>
|
||
|
||
<div className='flex flex-col gap-5'>
|
||
{sections.map((section) => (
|
||
<div key={section.key} className='flex flex-col'>
|
||
{section.title && (
|
||
<>
|
||
<span className='text-[var(--text-primary)] text-small'>{section.title}</span>
|
||
<div className='mt-2 mb-2.5 h-px bg-[var(--border)]' />
|
||
</>
|
||
)}
|
||
<div className='flex flex-col gap-2.5'>
|
||
{section.rows.map((row) => (
|
||
<div key={row.label} className='flex items-center justify-between gap-3'>
|
||
<span className='text-[var(--text-body)] text-sm'>{row.label}</span>
|
||
<FeatureValue value={row.value} />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</article>
|
||
)
|
||
}
|