Merge pull request #9 from ciphera-net/staging
[PULSE-10] Billing usage in dashboard – frontend
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
41
app/page.tsx
41
app/page.tsx
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}, [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>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user