feat: add free plan to pricing page and enforce 1-site limit
Show the free tier (€0, 1 site, 5k pageviews, 6 months retention) as the first card on the pricing page. Enforce a 1-site limit for free plan users in the frontend.
This commit is contained in:
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Free plan now visible on the Pricing page.** The free tier is no longer hidden — it's displayed as the first option on the Pricing page so you can see exactly what you get before signing up: 1 site, 5,000 monthly pageviews, and 6 months of data retention, completely free.
|
||||
- **Free plan limited to 1 site.** Free accounts are now limited to a single site. If you need more, you can upgrade to Solo or above from the Pricing page.
|
||||
|
||||
### Improved
|
||||
|
||||
- **Sites now show their verification status.** Each site on your dashboard now displays either a green "Active" badge (if verified) or an amber "Unverified" badge. When you verify your tracking script installation, the status is saved permanently — no more showing "Active" for sites that haven't been set up yet.
|
||||
|
||||
@@ -292,7 +292,42 @@ export default function PricingSection() {
|
||||
</div>
|
||||
|
||||
{/* Pricing Grid */}
|
||||
<div className="grid md:grid-cols-4 divide-y md:divide-y-0 md:divide-x divide-neutral-200 dark:divide-neutral-800">
|
||||
<div className="grid md:grid-cols-5 divide-y md:divide-y-0 md:divide-x divide-neutral-200 dark:divide-neutral-800">
|
||||
{/* Free Plan */}
|
||||
<div className="p-6 flex flex-col relative transition-colors hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50">
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold text-neutral-900 dark:text-white mb-2">Free</h3>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 min-h-[40px] mb-4">For trying Pulse on a personal project</p>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-4xl font-bold text-neutral-900 dark:text-white">€0</span>
|
||||
<span className="text-neutral-500 dark:text-neutral-400 font-medium">/forever</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!user) {
|
||||
initiateOAuthFlow()
|
||||
return
|
||||
}
|
||||
window.location.href = '/'
|
||||
}}
|
||||
variant="secondary"
|
||||
className="w-full mb-8"
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
|
||||
<ul className="space-y-4 flex-grow">
|
||||
{['1 site', '5k monthly pageviews', '6 months data retention', '100% Data ownership'].map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-3 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<CheckCircleIcon className="w-5 h-5 shrink-0 text-neutral-400" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{PLANS.map((plan) => {
|
||||
const priceDetails = getPriceDetails(plan.id)
|
||||
const isTeam = plan.id === 'team'
|
||||
|
||||
@@ -7,9 +7,9 @@ export const PLAN_ID_SOLO = 'solo'
|
||||
export const PLAN_ID_TEAM = 'team'
|
||||
export const PLAN_ID_BUSINESS = 'business'
|
||||
|
||||
/** Sites limit per plan. Returns null for free (no limit enforced in UI). */
|
||||
/** Sites limit per plan. */
|
||||
export function getSitesLimitForPlan(planId: string | null | undefined): number | null {
|
||||
if (!planId || planId === 'free') return null
|
||||
if (!planId || planId === 'free') return 1
|
||||
switch (planId) {
|
||||
case 'solo': return 1
|
||||
case 'team': return 5
|
||||
|
||||
Reference in New Issue
Block a user