refactor(www): extract shared markdown table builder, tighten pricing catalog types

This commit is contained in:
Pamela Chia committed 2026-08-26 17:34:41 +08:00
1 parent af435025ac
commit b522f31060
4 files changed
+21 -46

No files matched your search

+15 -31
View File
@@ -31,6 +31,16 @@ function pad(str: string, len: number): string {
return str.padEnd(len)
}
function buildMarkdownTable(headers: string[], dataRows: string[][]): string[] {
const widths = headers.map((h, i) => Math.max(h.length, ...dataRows.map((r) => r[i].length)))
const headerRow = `| ${headers.map((h, i) => pad(h, widths[i])).join(' | ')} |`
const separator = `| ${widths.map((w) => '-'.repeat(w)).join(' | ')} |`
const bodyRows = dataRows.map(
(cells) => `| ${cells.map((c, i) => pad(c, widths[i])).join(' | ')} |`
)
return [headerRow, separator, ...bodyRows]
}
// ---------------------------------------------------------------------------
// Plan Tiers
// ---------------------------------------------------------------------------
@@ -97,22 +107,12 @@ function buildComputeSection(): string {
})
)
const widths = headers.map((h, i) => Math.max(h.length, ...dataRows.map((r) => r[i].length)))
const headerRow = `| ${headers.map((h, i) => pad(h, widths[i])).join(' | ')} |`
const separator = `| ${widths.map((w) => '-'.repeat(w)).join(' | ')} |`
const bodyRows = dataRows.map(
(cells) => `| ${cells.map((c, i) => pad(c, widths[i])).join(' | ')} |`
)
return [
'## Compute Add-Ons',
'',
'All projects run on a compute instance. Pro and Team plans include Micro compute in the base price.',
'',
headerRow,
separator,
...bodyRows,
...buildMarkdownTable(headers, dataRows),
'',
`Compute is billed hourly. Each project runs its own instance. Pro and Team plans include ${usd(PLAN_BILLING.pro.computeCreditsMonthly)}/month in compute credits (covers one Micro instance). Additional projects add their full compute cost.`,
'',
@@ -126,7 +126,7 @@ function buildComputeSection(): string {
function buildDiskSection(): string {
const headers = ['Disk type', 'Max size', 'Size', 'IOPS', 'Throughput', 'Durability']
const dataRows = Object.values(DISK_PRICING).map((disk) => {
const dataRows = Object.entries(DISK_PRICING).map(([type, disk]) => {
const size = disk.includedPerProject
? `${disk.includedPerProject.sizeGb} GB included, then ${usd(disk.perUnitMonth.sizeGb)} per GB`
: `${usd(disk.perUnitMonth.sizeGb)} per GB`
@@ -139,7 +139,7 @@ function buildDiskSection(): string {
: (disk.throughputNote ?? '')
return [
`${disk.displayName} (${disk.type})`,
`${disk.displayName} (${type})`,
`${disk.maxSizeTb} TB`,
size,
iops,
@@ -148,14 +148,7 @@ function buildDiskSection(): string {
]
})
const widths = headers.map((h, i) => Math.max(h.length, ...dataRows.map((r) => r[i].length)))
const headerRow = `| ${headers.map((h, i) => pad(h, widths[i])).join(' | ')} |`
const separator = `| ${widths.map((w) => '-'.repeat(w)).join(' | ')} |`
const bodyRows = dataRows.map(
(cells) => `| ${cells.map((c, i) => pad(c, widths[i])).join(' | ')} |`
)
return ['## Disk Storage', '', headerRow, separator, ...bodyRows, ''].join('\n')
return ['## Disk Storage', '', ...buildMarkdownTable(headers, dataRows), ''].join('\n')
}
// ---------------------------------------------------------------------------
@@ -192,16 +185,7 @@ function buildAddOnsSection(): string {
],
]
const nameWidth = Math.max('Add-on'.length, ...addOns.map(([n]) => n.length))
const priceWidth = Math.max('Price'.length, ...addOns.map(([, p]) => p.length))
const header = `| ${pad('Add-on', nameWidth)} | ${pad('Price', priceWidth)} |`
const separator = `| ${'-'.repeat(nameWidth)} | ${'-'.repeat(priceWidth)} |`
const rows = addOns.map(
([name, price]) => `| ${pad(name, nameWidth)} | ${pad(price, priceWidth)} |`
)
return ['## Add-Ons', '', header, separator, ...rows, ''].join('\n')
return ['## Add-Ons', '', ...buildMarkdownTable(['Add-on', 'Price'], addOns), ''].join('\n')
}
// ---------------------------------------------------------------------------
+2 -2
View File
@@ -87,8 +87,8 @@ export function buildPricingJson() {
},
disk: {
scope: 'per-project',
types: Object.values(DISK_PRICING).map((disk) => ({
type: disk.type,
types: Object.entries(DISK_PRICING).map(([type, disk]) => ({
type,
name: disk.displayName,
maxSizeTb: disk.maxSizeTb,
durabilityPercent: disk.durabilityPercent,
+2 -1
View File
@@ -1,6 +1,7 @@
import { ADDONS, gbDisplay, METERS, PLAN_BILLING, qty, usd, withUnit } from './pricing-catalog'
import type { PlanKey } from './pricing-catalog'
export type PlanId = 'free' | 'pro' | 'team' | 'enterprise'
export type PlanId = PlanKey
export interface PricingInformation {
id: 'tier_free' | 'tier_pro' | 'tier_team' | 'tier_enterprise'
+2 -12
View File
@@ -26,7 +26,6 @@ export interface DiskAllowance {
}
export interface DiskTypeEntry {
type: 'gp3' | 'io2'
displayName: string
tagline: string
maxSizeTb: number
@@ -36,13 +35,6 @@ export interface DiskTypeEntry {
throughputNote?: string
}
export interface AddonAvailability {
free: boolean
pro: boolean
team: boolean
enterprise: boolean
}
export function usd(amount: number, decimals?: number): string {
return `$${decimals === undefined ? String(amount) : amount.toFixed(decimals)}`
}
@@ -72,9 +64,8 @@ export const PLAN_BILLING = {
enterprise: { priceMonthly: null, computeCreditsMonthly: null, spendCapAvailable: false },
} as const satisfies Record<PlanKey, PlanBillingEntry>
export const DISK_PRICING: Record<'gp3' | 'io2', DiskTypeEntry> = {
export const DISK_PRICING = {
gp3: {
type: 'gp3',
displayName: 'General Purpose',
tagline: 'Balance between price and performance',
maxSizeTb: 16,
@@ -83,7 +74,6 @@ export const DISK_PRICING: Record<'gp3' | 'io2', DiskTypeEntry> = {
perUnitMonth: { sizeGb: GP3_PER_GB_MONTH, iops: 0.024, throughputMBps: 0.095 },
},
io2: {
type: 'io2',
displayName: 'High Performance',
tagline: 'For mission critical applications',
maxSizeTb: 60,
@@ -92,7 +82,7 @@ export const DISK_PRICING: Record<'gp3' | 'io2', DiskTypeEntry> = {
perUnitMonth: { sizeGb: 0.195, iops: 0.119, throughputMBps: null },
throughputNote: 'Scales automatically with IOPS',
},
}
} as const satisfies Record<'gp3' | 'io2', DiskTypeEntry>
export const METERS = {
egress: {