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]
|
## [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
|
### 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.
|
- **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>
|
</div>
|
||||||
|
|
||||||
{/* Pricing Grid */}
|
{/* 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) => {
|
{PLANS.map((plan) => {
|
||||||
const priceDetails = getPriceDetails(plan.id)
|
const priceDetails = getPriceDetails(plan.id)
|
||||||
const isTeam = plan.id === 'team'
|
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_TEAM = 'team'
|
||||||
export const PLAN_ID_BUSINESS = 'business'
|
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 {
|
export function getSitesLimitForPlan(planId: string | null | undefined): number | null {
|
||||||
if (!planId || planId === 'free') return null
|
if (!planId || planId === 'free') return 1
|
||||||
switch (planId) {
|
switch (planId) {
|
||||||
case 'solo': return 1
|
case 'solo': return 1
|
||||||
case 'team': return 5
|
case 'team': return 5
|
||||||
|
|||||||
Reference in New Issue
Block a user