init
This commit is contained in:
commit
d69342b2e9
160 changed files with 28681 additions and 0 deletions
251
studio/src/pages/BillingPage.tsx
Normal file
251
studio/src/pages/BillingPage.tsx
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { useState } from 'react'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { PageHeader, BillingPageSkeleton } from '../components/shared'
|
||||
import { Icons } from '../components/shared/Icons'
|
||||
import { Button } from '../components/ui'
|
||||
import { $billing } from '../stores/billing'
|
||||
import type { Tier, TierConfig } from '../types'
|
||||
|
||||
type BillingCycle = 'monthly' | 'annual'
|
||||
|
||||
function formatPrice(cents: number): string {
|
||||
return `$${(cents / 100).toFixed(0)}`
|
||||
}
|
||||
|
||||
function getFeatureList(config: TierConfig, tier: Tier): { name: string; included: boolean }[] {
|
||||
if (tier === 'free') {
|
||||
return [
|
||||
{ name: 'Unlimited posts', included: true },
|
||||
{ name: 'Comments & reactions', included: true },
|
||||
{ name: 'writekit.dev subdomain', included: true },
|
||||
{ name: `${config.analytics_retention}-day analytics`, included: true },
|
||||
{ name: `API access (${config.api_rate_limit} req/hr)`, included: true },
|
||||
{ name: `${config.max_webhooks} webhooks`, included: true },
|
||||
{ name: `${config.max_plugins} plugins`, included: true },
|
||||
{ name: 'Custom domain', included: false },
|
||||
{ name: 'Remove "Powered by" badge', included: false },
|
||||
]
|
||||
}
|
||||
return [
|
||||
{ name: 'Unlimited posts', included: true },
|
||||
{ name: 'Comments & reactions', included: true },
|
||||
{ name: 'writekit.dev subdomain', included: true },
|
||||
{ name: 'Custom domain', included: true },
|
||||
{ name: 'No "Powered by" badge', included: true },
|
||||
{ name: `${config.analytics_retention}-day analytics`, included: true },
|
||||
{ name: `API access (${config.api_rate_limit} req/hr)`, included: true },
|
||||
{ name: `${config.max_webhooks} webhooks`, included: true },
|
||||
{ name: `${config.max_plugins} plugins`, included: true },
|
||||
{ name: 'Priority support', included: true },
|
||||
]
|
||||
}
|
||||
|
||||
export default function BillingPage() {
|
||||
const { data: billing } = useStore($billing)
|
||||
const [billingCycle, setBillingCycle] = useState<BillingCycle>('annual')
|
||||
|
||||
if (!billing) return <BillingPageSkeleton />
|
||||
|
||||
const currentTier = billing.current_tier
|
||||
const currentConfig = billing.tiers[currentTier]
|
||||
const proConfig = billing.tiers.pro
|
||||
const proFeatures = getFeatureList(proConfig, 'pro')
|
||||
|
||||
const annualSavings = (proConfig.monthly_price * 12 - proConfig.annual_price) / 100
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader />
|
||||
|
||||
<div className="-mx-6 lg:-mx-10 mt-6">
|
||||
{/* Current Plan */}
|
||||
<div className="px-6 lg:px-10 py-5">
|
||||
<div className="text-xs font-medium text-muted uppercase tracking-wide">Current Plan</div>
|
||||
</div>
|
||||
<div className="px-6 lg:px-10 pb-6">
|
||||
<div className="p-4 border border-accent bg-accent/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-medium">{currentConfig.name}</span>
|
||||
<span className="px-2 py-0.5 bg-accent/20 text-accent text-xs font-medium">Active</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted mt-1">{currentConfig.description}</div>
|
||||
</div>
|
||||
{currentTier === 'free' && (
|
||||
<Button variant="primary" href="#upgrade">Upgrade to Pro</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Upgrade Section */}
|
||||
{currentTier === 'free' && (
|
||||
<>
|
||||
<div id="upgrade" className="px-6 lg:px-10 py-5">
|
||||
<div className="text-xs font-medium text-muted uppercase tracking-wide">Upgrade to Pro</div>
|
||||
<div className="text-xs text-muted mt-0.5">Get custom domain, extended analytics, and more</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 lg:px-10 pb-6">
|
||||
{/* Billing Toggle */}
|
||||
<div className="flex items-center justify-center gap-3 mb-6">
|
||||
<button
|
||||
onClick={() => setBillingCycle('monthly')}
|
||||
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
|
||||
billingCycle === 'monthly' ? 'text-text' : 'text-muted hover:text-text'
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBillingCycle('annual')}
|
||||
className={`px-3 py-1.5 text-sm font-medium transition-colors flex items-center gap-2 ${
|
||||
billingCycle === 'annual' ? 'text-text' : 'text-muted hover:text-text'
|
||||
}`}
|
||||
>
|
||||
Annual
|
||||
<span className="px-1.5 py-0.5 bg-success/20 text-success text-xs">
|
||||
Save ${annualSavings}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Pro Plan Card */}
|
||||
<div className="max-w-md mx-auto p-6 border border-border">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-sm font-medium text-muted mb-2">Pro</div>
|
||||
<div className="text-4xl font-semibold tracking-tight">
|
||||
{formatPrice(billingCycle === 'monthly' ? proConfig.monthly_price : proConfig.annual_price)}
|
||||
<span className="text-base font-normal text-muted">
|
||||
/{billingCycle === 'monthly' ? 'mo' : 'yr'}
|
||||
</span>
|
||||
</div>
|
||||
{billingCycle === 'annual' && (
|
||||
<div className="text-xs text-muted mt-1">
|
||||
{formatPrice(Math.round(proConfig.annual_price / 12))}/mo billed annually
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3 mb-6">
|
||||
{proFeatures.map((feature) => (
|
||||
<li key={feature.name} className="flex items-center gap-2 text-sm">
|
||||
<Icons.Check className="text-success flex-shrink-0" />
|
||||
<span>{feature.name}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Button variant="primary" className="w-full">
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-muted text-center mt-3">
|
||||
Secure payment via Lemon Squeezy. Cancel anytime.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Feature Comparison */}
|
||||
<div className="px-6 lg:px-10 py-5">
|
||||
<div className="text-xs font-medium text-muted uppercase tracking-wide">Feature Comparison</div>
|
||||
</div>
|
||||
<div className="px-6 lg:px-10 pb-6">
|
||||
<div className="border border-border overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/5">
|
||||
<th className="text-left px-4 py-3 font-medium">Feature</th>
|
||||
<th className="text-center px-4 py-3 font-medium w-24">Free</th>
|
||||
<th className="text-center px-4 py-3 font-medium w-24">Pro</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-border">
|
||||
<td className="px-4 py-3">Custom domain</td>
|
||||
<td className="text-center px-4 py-3"><Icons.Close className="text-muted/50 inline" /></td>
|
||||
<td className="text-center px-4 py-3"><Icons.Check className="text-success inline" /></td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="px-4 py-3">"Powered by WriteKit" badge</td>
|
||||
<td className="text-center px-4 py-3 text-muted">Required</td>
|
||||
<td className="text-center px-4 py-3 text-muted">Hidden</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="px-4 py-3">Analytics retention</td>
|
||||
<td className="text-center px-4 py-3 text-muted">{billing.tiers.free.analytics_retention} days</td>
|
||||
<td className="text-center px-4 py-3 text-muted">{billing.tiers.pro.analytics_retention} days</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="px-4 py-3">API rate limit</td>
|
||||
<td className="text-center px-4 py-3 text-muted">{billing.tiers.free.api_rate_limit}/hr</td>
|
||||
<td className="text-center px-4 py-3 text-muted">{billing.tiers.pro.api_rate_limit}/hr</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="px-4 py-3">Webhooks</td>
|
||||
<td className="text-center px-4 py-3 text-muted">{billing.tiers.free.max_webhooks}</td>
|
||||
<td className="text-center px-4 py-3 text-muted">{billing.tiers.pro.max_webhooks}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="px-4 py-3">Plugins</td>
|
||||
<td className="text-center px-4 py-3 text-muted">{billing.tiers.free.max_plugins}</td>
|
||||
<td className="text-center px-4 py-3 text-muted">{billing.tiers.pro.max_plugins}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border">
|
||||
<td className="px-4 py-3">Posts</td>
|
||||
<td className="text-center px-4 py-3 text-muted">Unlimited</td>
|
||||
<td className="text-center px-4 py-3 text-muted">Unlimited</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-4 py-3">Support</td>
|
||||
<td className="text-center px-4 py-3 text-muted">Community</td>
|
||||
<td className="text-center px-4 py-3 text-muted">Priority</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* FAQ */}
|
||||
<div className="px-6 lg:px-10 py-5">
|
||||
<div className="text-xs font-medium text-muted uppercase tracking-wide">Questions</div>
|
||||
</div>
|
||||
<div className="px-6 lg:px-10 pb-6 space-y-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1">Can I cancel anytime?</div>
|
||||
<div className="text-sm text-muted">
|
||||
Yes. Cancel anytime and keep access until the end of your billing period.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1">Can I switch from monthly to annual?</div>
|
||||
<div className="text-sm text-muted">
|
||||
Yes. Switch anytime and we'll prorate your payment.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1">What happens to my content if I downgrade?</div>
|
||||
<div className="text-sm text-muted">
|
||||
Your content stays. Custom domain will stop working, badge will appear, and analytics older than 7 days won't be accessible.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1">Can I export my data?</div>
|
||||
<div className="text-sm text-muted">
|
||||
Yes. Export all your posts, settings, and assets anytime from the Data page. Your data is always yours.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue