mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-08 06:42:39 +02:00
billing + credits updates (#202)
This commit is contained in:
parent
852e02e49e
commit
eccfb4748f
13 changed files with 497 additions and 229 deletions
|
|
@ -3,7 +3,7 @@
|
|||
import { Progress, Badge, Chip } from "@heroui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/app/lib/components/label";
|
||||
import { Customer, UsageResponse, UsageType } from "@/app/lib/types/billing_types";
|
||||
import { Customer, UsageResponse } from "@/app/lib/types/billing_types";
|
||||
import { z } from "zod";
|
||||
import { tokens } from "@/app/styles/design-tokens";
|
||||
import { SectionHeading } from "@/components/ui/section-heading";
|
||||
|
|
@ -47,6 +47,15 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
const displayStatus = getDisplayStatus(customer.subscriptionStatus);
|
||||
const planInfo = planDetails[plan];
|
||||
|
||||
// Prepare usage metrics data
|
||||
const usageData = Object.entries(usage.usage)
|
||||
.map(([type, credits]) => ({
|
||||
type,
|
||||
credits,
|
||||
totalUsedCredits: usage.sanctionedCredits - usage.availableCredits
|
||||
}))
|
||||
.sort((a, b) => b.credits - a.credits);
|
||||
|
||||
async function handleManageSubscription() {
|
||||
const returnUrl = new URL('/billing/callback', window.location.origin);
|
||||
returnUrl.searchParams.set('redirect', window.location.href);
|
||||
|
|
@ -109,48 +118,175 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* Usage Metrics Panel */}
|
||||
{/* Credits Overview Panel */}
|
||||
<section className="card">
|
||||
<div className="px-4 pt-4 pb-6">
|
||||
<SectionHeading>
|
||||
Usage Metrics
|
||||
Credits Overview
|
||||
</SectionHeading>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
<div className="p-6 space-y-6">
|
||||
{Object.entries(usage.usage).map(([type, { usage: used, total }]) => {
|
||||
const usageType = type as z.infer<typeof UsageType>;
|
||||
const percentage = Math.min((used / total) * 100, 100);
|
||||
const isOverLimit = used > total;
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label label="Sanctioned Credits" />
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.lg,
|
||||
tokens.typography.weights.semibold,
|
||||
tokens.colors.light.text.primary,
|
||||
tokens.colors.dark.text.primary
|
||||
)}>
|
||||
{usage.sanctionedCredits.toLocaleString()}
|
||||
</p>
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
tokens.colors.light.text.secondary,
|
||||
tokens.colors.dark.text.secondary
|
||||
)}>
|
||||
Total credits allocated to your plan
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label label="Used Credits" />
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.lg,
|
||||
tokens.typography.weights.semibold,
|
||||
tokens.colors.light.text.primary,
|
||||
tokens.colors.dark.text.primary
|
||||
)}>
|
||||
{(usage.sanctionedCredits - usage.availableCredits).toLocaleString()}
|
||||
</p>
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
tokens.colors.light.text.secondary,
|
||||
tokens.colors.dark.text.secondary
|
||||
)}>
|
||||
Credits consumed so far
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label label="Available Credits" />
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.lg,
|
||||
tokens.typography.weights.semibold,
|
||||
usage.availableCredits < 0 ? "text-red-500" : clsx(
|
||||
tokens.colors.light.text.primary,
|
||||
tokens.colors.dark.text.primary
|
||||
)
|
||||
)}>
|
||||
{usage.availableCredits.toLocaleString()}
|
||||
</p>
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
tokens.colors.light.text.secondary,
|
||||
tokens.colors.dark.text.secondary
|
||||
)}>
|
||||
Credits remaining for use
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Warning for negative credits */}
|
||||
{usage.availableCredits < 0 && (
|
||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
"text-red-700 dark:text-red-300"
|
||||
)}>
|
||||
⚠️ You have exceeded your credit limit. Please upgrade your plan or contact support to avoid service interruptions.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warning for high credit usage (>80%) */}
|
||||
{usage.availableCredits >= 0 && ((usage.sanctionedCredits - usage.availableCredits) / usage.sanctionedCredits) > 0.8 && (
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
"text-yellow-700 dark:text-yellow-300"
|
||||
)}>
|
||||
⚠️ You have used more than 80% of your credits. Consider upgrading your plan to avoid interruptions.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Credits Progress Bar */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label label="Credits Usage" />
|
||||
<span className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
tokens.colors.light.text.secondary,
|
||||
tokens.colors.dark.text.secondary
|
||||
)}>
|
||||
{Math.round(((usage.sanctionedCredits - usage.availableCredits) / usage.sanctionedCredits) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
size="lg"
|
||||
value={((usage.sanctionedCredits - usage.availableCredits) / usage.sanctionedCredits) * 100}
|
||||
color={usage.availableCredits < 0 ? "danger" : "primary"}
|
||||
className="h-4"
|
||||
aria-label="Credits usage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
return (
|
||||
<div key={type} className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<Label label={type.replace(/_/g, ' ')} />
|
||||
<p className={clsx(
|
||||
{/* Usage Metrics Panel */}
|
||||
<section className="card">
|
||||
<div className="px-4 pt-4 pb-6">
|
||||
<SectionHeading>
|
||||
Usage data
|
||||
</SectionHeading>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
<div className="p-6 space-y-6">
|
||||
{usageData.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
tokens.colors.light.text.secondary,
|
||||
tokens.colors.dark.text.secondary
|
||||
)}>
|
||||
No usage data yet
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
usageData.map(({ type, credits, totalUsedCredits }) => {
|
||||
const percentage = totalUsedCredits > 0 ? (credits / totalUsedCredits) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div key={type} className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-1">
|
||||
<Label label={type.replace(/_/g, ' ')} />
|
||||
<p className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
tokens.colors.light.text.secondary,
|
||||
tokens.colors.dark.text.secondary
|
||||
)}>
|
||||
{credits.toLocaleString()} credits
|
||||
</p>
|
||||
</div>
|
||||
<span className={clsx(
|
||||
tokens.typography.sizes.sm,
|
||||
tokens.colors.light.text.secondary,
|
||||
tokens.colors.dark.text.secondary
|
||||
)}>
|
||||
{used.toLocaleString()} / {total.toLocaleString()}
|
||||
</p>
|
||||
{Math.round(percentage)}%
|
||||
</span>
|
||||
</div>
|
||||
{isOverLimit && (
|
||||
<Badge color="danger" variant="flat">
|
||||
Over Limit
|
||||
</Badge>
|
||||
)}
|
||||
<Progress
|
||||
value={percentage}
|
||||
color="default"
|
||||
className="h-2"
|
||||
aria-label={`${type} credits usage`}
|
||||
/>
|
||||
</div>
|
||||
<Progress
|
||||
value={percentage}
|
||||
color={isOverLimit ? "danger" : "primary"}
|
||||
className="h-2"
|
||||
aria-label={`${type} usage`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue