feat: implement site limits based on subscription plans across dashboard and new site creation; enhance UI feedback for plan limits
This commit is contained in:
16
app/page.tsx
16
app/page.tsx
@@ -13,6 +13,7 @@ import { Button } from '@ciphera-net/ui'
|
||||
import { BarChartIcon, LockIcon, ZapIcon, CheckCircleIcon, XIcon, GlobeIcon } from '@ciphera-net/ui'
|
||||
import { toast } from '@ciphera-net/ui'
|
||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||
import { getSitesLimitForPlan } from '@/lib/plans'
|
||||
|
||||
function DashboardPreview() {
|
||||
return (
|
||||
@@ -337,10 +338,13 @@ export default function HomePage() {
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">Your Sites</h1>
|
||||
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">Manage your analytics sites and view insights.</p>
|
||||
</div>
|
||||
{subscription?.plan_id === 'solo' && sites.length >= 1 ? (
|
||||
{(() => {
|
||||
const siteLimit = getSitesLimitForPlan(subscription?.plan_id)
|
||||
const atLimit = siteLimit != null && sites.length >= siteLimit
|
||||
return atLimit ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-3 py-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||
Limit reached (1/1)
|
||||
Limit reached ({sites.length}/{siteLimit})
|
||||
</span>
|
||||
<Link href="/pricing">
|
||||
<Button variant="primary" className="text-sm">
|
||||
@@ -348,7 +352,8 @@ export default function HomePage() {
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
) : null
|
||||
})() ?? (
|
||||
<Link href="/sites/new">
|
||||
<Button variant="primary" className="text-sm">
|
||||
Add New Site
|
||||
@@ -388,7 +393,10 @@ export default function HomePage() {
|
||||
{(typeof subscription.sites_count === 'number' || (subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number')) && (
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
||||
{typeof subscription.sites_count === 'number' && (
|
||||
<span>Sites: {subscription.plan_id === 'solo' && subscription.sites_count > 0 ? `${subscription.sites_count}/1` : subscription.sites_count}</span>
|
||||
<span>Sites: {(() => {
|
||||
const limit = getSitesLimitForPlan(subscription.plan_id)
|
||||
return limit != null && typeof subscription.sites_count === 'number' ? `${subscription.sites_count}/${limit}` : subscription.sites_count
|
||||
})()}</span>
|
||||
)}
|
||||
{typeof subscription.sites_count === 'number' && subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && ' · '}
|
||||
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { createSite, listSites, getSite, type Site } from '@/lib/api/sites'
|
||||
import { getSubscription } from '@/lib/api/billing'
|
||||
import { getSitesLimitForPlan } from '@/lib/plans'
|
||||
import { trackSiteCreatedFromDashboard, trackSiteCreatedScriptCopied } from '@/lib/welcomeAnalytics'
|
||||
import { toast } from '@ciphera-net/ui'
|
||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||
@@ -57,9 +58,10 @@ export default function NewSitePage() {
|
||||
getSubscription()
|
||||
])
|
||||
|
||||
if (subscription?.plan_id === 'solo' && sites.length >= 1) {
|
||||
const siteLimit = subscription?.plan_id ? getSitesLimitForPlan(subscription.plan_id) : null
|
||||
if (siteLimit != null && sites.length >= siteLimit) {
|
||||
setAtLimit(true)
|
||||
toast.error('Solo plan limit reached (1 site). Please upgrade to add more sites.')
|
||||
toast.error(`${subscription.plan_id} plan limit reached (${siteLimit} site${siteLimit === 1 ? '' : 's'}). Please upgrade to add more sites.`)
|
||||
router.replace('/')
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Organization
|
||||
} from '@/lib/api/organization'
|
||||
import { getSubscription, createPortalSession, getInvoices, cancelSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Invoice } from '@/lib/api/billing'
|
||||
import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex } from '@/lib/plans'
|
||||
import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex, getSitesLimitForPlan } from '@/lib/plans'
|
||||
import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit'
|
||||
import { getNotificationSettings, updateNotificationSettings } from '@/lib/api/notification-settings'
|
||||
import { toast } from '@ciphera-net/ui'
|
||||
@@ -863,9 +863,10 @@ export default function OrganizationSettings() {
|
||||
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Sites</div>
|
||||
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
{typeof subscription.sites_count === 'number'
|
||||
? subscription.plan_id === 'solo'
|
||||
? `${subscription.sites_count} / 1`
|
||||
: `${subscription.sites_count}`
|
||||
? (() => {
|
||||
const limit = getSitesLimitForPlan(subscription.plan_id)
|
||||
return limit != null ? `${subscription.sites_count} / ${limit}` : `${subscription.sites_count}`
|
||||
})()
|
||||
: '—'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
15
lib/plans.ts
15
lib/plans.ts
@@ -1,9 +1,22 @@
|
||||
/**
|
||||
* Shared plan and traffic tier definitions for pricing and billing (Change plan).
|
||||
* Backend supports plan_id "solo" and limit 10k–10M; month/year interval.
|
||||
* Backend supports plan_id solo, team, business and limit 10k–10M; month/year interval.
|
||||
*/
|
||||
|
||||
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). */
|
||||
export function getSitesLimitForPlan(planId: string | null | undefined): number | null {
|
||||
if (!planId || planId === 'free') return null
|
||||
switch (planId) {
|
||||
case 'solo': return 1
|
||||
case 'team': return 5
|
||||
case 'business': return 10
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Traffic tiers available for Solo plan (pageview limits). */
|
||||
export const TRAFFIC_TIERS = [
|
||||
|
||||
Reference in New Issue
Block a user