[PULSE-10] Billing usage in dashboard – frontend #9

Merged
uz1mani merged 3 commits from staging into main 2026-02-05 11:06:33 +00:00
4 changed files with 75 additions and 12 deletions

View File

@@ -1,3 +1,4 @@
import { Suspense } from 'react'
import OrganizationSettings from '@/components/settings/OrganizationSettings'
export const metadata = {
@@ -9,7 +10,9 @@ export default function OrgSettingsPage() {
return (
<div className="min-h-screen pt-24 pb-12 px-4 sm:px-6">
<div className="max-w-4xl mx-auto">
<OrganizationSettings />
<Suspense fallback={<div className="p-8 text-center text-neutral-500">Loading...</div>}>
<OrganizationSettings />
</Suspense>
</div>
</div>
)

View File

@@ -285,12 +285,13 @@ export default function HomePage() {
<p className="text-2xl font-bold text-neutral-900 dark:text-white">--</p>
</div>
<div className="rounded-xl border border-neutral-200 bg-brand-orange/10 p-4 dark:border-neutral-800">
<p className="text-sm text-brand-orange">Plan Status</p>
<p className="text-lg font-bold text-brand-orange">
{subscriptionLoading
? '...'
: (() => {
if (!subscription) return 'Free Plan'
<p className="text-sm text-brand-orange">Plan & usage</p>
{subscriptionLoading ? (
<p className="text-lg font-bold text-brand-orange">...</p>
) : subscription ? (
<>
<p className="text-lg font-bold text-brand-orange">
{(() => {
const raw =
subscription.plan_id?.startsWith('price_')
? 'Pro'
@@ -300,7 +301,33 @@ export default function HomePage() {
const label = raw === 'Free' || raw === 'Pro' ? raw : raw.charAt(0).toUpperCase() + raw.slice(1)
return `${label} Plan`
})()}
</p>
</p>
{(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>
)}
{typeof subscription.sites_count === 'number' && subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && ' · '}
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && (
<span>Pageviews: {subscription.pageview_usage.toLocaleString()}/{subscription.pageview_limit.toLocaleString()}</span>
)}
</p>
)}
<div className="mt-2 flex gap-2">
{subscription.has_payment_method ? (
<Link href="/org-settings?tab=billing" className="text-sm font-medium text-brand-orange hover:underline">
Manage billing
</Link>
) : (
<Link href="/pricing" className="text-sm font-medium text-brand-orange hover:underline">
Upgrade
</Link>
)}
</div>
</>
) : (
<p className="text-lg font-bold text-brand-orange">Free Plan</p>
)}
</div>
</div>

View File

@@ -1,7 +1,7 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
import { useAuth } from '@/lib/auth/context'
import {
deleteOrganization,
@@ -38,7 +38,11 @@ import { Button, Input } from '@ciphera-net/ui'
export default function OrganizationSettings() {
const { user } = useAuth()
const router = useRouter()
const [activeTab, setActiveTab] = useState<'general' | 'members' | 'billing'>('general')
const searchParams = useSearchParams()
const [activeTab, setActiveTab] = useState<'general' | 'members' | 'billing'>(() => {
const tab = searchParams.get('tab')
return tab === 'billing' || tab === 'members' ? tab : 'general'
})
const [showDeletePrompt, setShowDeletePrompt] = useState(false)
const [deleteConfirm, setDeleteConfirm] = useState('')
@@ -135,6 +139,13 @@ export default function OrganizationSettings() {
}
greptile-apps[bot] commented 2026-02-05 10:49:01 +00:00 (Migrated from github.com)
Review

This useEffect can cause infinite re-renders if the URL updates programmatically. When setActiveTab is called, if other code updates the URL, it will trigger this effect again. Consider adding a guard or removing this effect since initial state is already set from searchParams in the useState initializer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/settings/OrganizationSettings.tsx
Line: 142:147

Comment:
This `useEffect` can cause infinite re-renders if the URL updates programmatically. When `setActiveTab` is called, if other code updates the URL, it will trigger this effect again. Consider adding a guard or removing this effect since initial state is already set from `searchParams` in the `useState` initializer.

How can I resolve this? If you propose a fix, please make it concise.
This `useEffect` can cause infinite re-renders if the URL updates programmatically. When `setActiveTab` is called, if other code updates the URL, it will trigger this effect again. Consider adding a guard or removing this effect since initial state is already set from `searchParams` in the `useState` initializer. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/settings/OrganizationSettings.tsx Line: 142:147 Comment: This `useEffect` can cause infinite re-renders if the URL updates programmatically. When `setActiveTab` is called, if other code updates the URL, it will trigger this effect again. Consider adding a guard or removing this effect since initial state is already set from `searchParams` in the `useState` initializer. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-05 10:55:41 +00:00 (Migrated from github.com)
Review

Change: Only call setActiveTab(tab) when the tab from the URL is different from the current activeTab.
How: Added && tab !== activeTab so we don’t set state when the URL and state already match, and added activeTab to the effect dependency array so the guard is correct.
Result: The effect no longer triggers repeated updates when the URL is updated programmatically; it only syncs when the URL tab actually changes (e.g. client-side navigation to ?tab=billing).

Change: Only call setActiveTab(tab) when the tab from the URL is different from the current activeTab. How: Added && tab !== activeTab so we don’t set state when the URL and state already match, and added activeTab to the effect dependency array so the guard is correct. Result: The effect no longer triggers repeated updates when the URL is updated programmatically; it only syncs when the URL tab actually changes (e.g. client-side navigation to ?tab=billing).
}, [currentOrgId, loadMembers])
useEffect(() => {
const tab = searchParams.get('tab')
if ((tab === 'billing' || tab === 'members') && tab !== activeTab) {
setActiveTab(tab)
}
}, [searchParams, activeTab])
useEffect(() => {
if (activeTab === 'billing' && currentOrgId) {
loadSubscription()
@@ -593,7 +604,25 @@ export default function OrganizationSettings() {
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 pt-6 border-t border-neutral-200 dark:border-neutral-800">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 pt-6 border-t border-neutral-200 dark:border-neutral-800">
<div>
<div className="text-sm text-neutral-500 mb-1">Sites</div>
<div className="font-medium text-neutral-900 dark:text-white">
{typeof subscription.sites_count === 'number'
? subscription.plan_id === 'solo'
? `${subscription.sites_count} / 1`
: `${subscription.sites_count}`
: ''}
</div>
</div>
<div>
<div className="text-sm text-neutral-500 mb-1">Pageviews this period</div>
<div className="font-medium text-neutral-900 dark:text-white">
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number'
? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}`
: ''}
</div>
</div>
<div>
<div className="text-sm text-neutral-500 mb-1">Billing Interval</div>
<div className="font-medium text-neutral-900 dark:text-white capitalize">
@@ -603,7 +632,7 @@ export default function OrganizationSettings() {
<div>
<div className="text-sm text-neutral-500 mb-1">Pageview Limit</div>
<div className="font-medium text-neutral-900 dark:text-white">
{subscription.pageview_limit.toLocaleString()} / month
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / month` : 'Unlimited'}
</div>
</div>
<div>

View File

@@ -7,6 +7,10 @@ export interface SubscriptionDetails {
billing_interval: string
pageview_limit: number
has_payment_method: boolean
/** Number of sites for the org (billing usage). Present when backend supports usage API. */
sites_count?: number
/** Pageviews in current billing period (when pageview_limit > 0). Present when backend supports usage API. */
pageview_usage?: number
}
async function billingFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {