Skip to content

Commit 6ad6f1a

Browse files
committed
cleanup billing pages
1 parent 72a080d commit 6ad6f1a

File tree

16 files changed

+1201
-1046
lines changed

16 files changed

+1201
-1046
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"use client";
2+
3+
import { CreditCardIcon } from "@phosphor-icons/react";
4+
import { usePathname } from "next/navigation";
5+
6+
const PAGE_TITLES = {
7+
"/billing": {
8+
title: "Usage & Metrics",
9+
description: "Monitor your usage and billing metrics",
10+
},
11+
"/billing/plans": {
12+
title: "Plans & Pricing",
13+
description: "Manage your subscription and billing plan",
14+
},
15+
"/billing/history": {
16+
title: "Payment History",
17+
description: "View your billing history and invoices",
18+
},
19+
};
20+
21+
const DEFAULT_TITLE = {
22+
title: "Billing & Subscription",
23+
description: "Manage your subscription, usage, and billing preferences",
24+
};
25+
26+
export function BillingHeader() {
27+
const pathname = usePathname();
28+
const { title, description } =
29+
PAGE_TITLES[pathname as keyof typeof PAGE_TITLES] ?? DEFAULT_TITLE;
30+
31+
return (
32+
<div className="border-b bg-linear-to-r from-background via-background to-muted/20">
33+
<div className="flex flex-col justify-between gap-3 p-4 sm:flex-row sm:items-center sm:gap-0 sm:px-6 sm:py-6">
34+
<div className="min-w-0 flex-1">
35+
<div className="flex items-center gap-4">
36+
<div className="rounded-xl border border-primary/20 bg-primary/10 p-3">
37+
<CreditCardIcon
38+
className="h-6 w-6 text-primary"
39+
size={24}
40+
weight="duotone"
41+
/>
42+
</div>
43+
<div>
44+
<h1 className="truncate font-bold text-2xl text-foreground tracking-tight sm:text-3xl">
45+
{title}
46+
</h1>
47+
<p className="mt-1 text-muted-foreground text-sm sm:text-base">
48+
{description}
49+
</p>
50+
</div>
51+
</div>
52+
</div>
53+
</div>
54+
</div>
55+
);
56+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"use client";
2+
3+
import { CreditCardIcon, WifiHighIcon } from "@phosphor-icons/react";
4+
import { usePersistentState } from "@/hooks/use-persistent-state";
5+
import { cn } from "@/lib/utils";
6+
import type { CustomerWithPaymentMethod } from "../types/billing";
7+
8+
type CreditCardDisplayProps = {
9+
customer: CustomerWithPaymentMethod | null;
10+
};
11+
12+
export function CreditCardDisplay({ customer }: CreditCardDisplayProps) {
13+
const [showCardDetails, setShowCardDetails] = usePersistentState<boolean>(
14+
"billing-card-details-visible",
15+
true
16+
);
17+
18+
const paymentMethod = customer?.payment_method;
19+
const card = paymentMethod?.card;
20+
21+
if (!card) {
22+
return (
23+
<div className="flex aspect-[1.586/1] w-full flex-col items-center justify-center rounded-xl border border-dashed bg-background">
24+
<CreditCardIcon
25+
className="mb-2 text-muted-foreground"
26+
size={28}
27+
weight="duotone"
28+
/>
29+
<span className="text-muted-foreground text-sm">No payment method</span>
30+
</div>
31+
);
32+
}
33+
34+
const cardHolder =
35+
paymentMethod?.billing_details?.name || customer?.name || "CARD HOLDER";
36+
const last4 = card.last4 || "****";
37+
const expMonth = card.exp_month?.toString().padStart(2, "0") || "00";
38+
const expYear = card.exp_year?.toString().slice(-2) || "00";
39+
const cardNumber = `•••• •••• •••• ${last4}`;
40+
const expiration = `${expMonth}/${expYear}`;
41+
const brand = (card.brand || "card").toLowerCase();
42+
43+
return (
44+
<div className="relative aspect-[1.586/1] w-full">
45+
<div
46+
className={cn(
47+
"absolute inset-0 flex flex-col justify-between overflow-hidden rounded-xl p-4",
48+
"bg-linear-to-tr from-foreground to-foreground/80",
49+
"before:pointer-events-none before:absolute before:inset-0 before:z-1 before:rounded-[inherit] before:ring-1 before:ring-white/20 before:ring-inset"
50+
)}
51+
>
52+
<div className="relative z-2 flex items-start justify-between">
53+
<WifiHighIcon
54+
className="rotate-90 text-white/80"
55+
size={20}
56+
weight="bold"
57+
/>
58+
<span className="font-semibold text-white/60 text-xs uppercase tracking-wider">
59+
{brand}
60+
</span>
61+
</div>
62+
63+
<div className="relative z-2 flex flex-col gap-2">
64+
{showCardDetails ? (
65+
<>
66+
<div className="flex items-end gap-2">
67+
<p className="font-semibold text-white/80 text-xs uppercase tracking-wide">
68+
{cardHolder}
69+
</p>
70+
<p className="ml-auto font-semibold text-white/80 text-xs tabular-nums">
71+
{expiration}
72+
</p>
73+
</div>
74+
<div className="flex items-end justify-between gap-3">
75+
<button
76+
aria-label="Hide card details"
77+
className="cursor-pointer font-semibold text-white tabular-nums tracking-wider transition-opacity hover:opacity-80"
78+
onClick={() => setShowCardDetails(false)}
79+
type="button"
80+
>
81+
{cardNumber}
82+
</button>
83+
<CardBrandLogo brand={brand} />
84+
</div>
85+
</>
86+
) : (
87+
<>
88+
<div className="flex items-end gap-2">
89+
<p className="font-semibold text-white/40 text-xs uppercase tracking-wide">
90+
•••• ••••
91+
</p>
92+
<p className="ml-auto font-semibold text-white/40 text-xs tabular-nums">
93+
••/••
94+
</p>
95+
</div>
96+
<div className="flex items-end justify-between gap-3">
97+
<button
98+
aria-label="Show card details"
99+
className="cursor-pointer font-semibold text-white/40 tabular-nums tracking-wider transition-opacity hover:opacity-80"
100+
onClick={() => setShowCardDetails(true)}
101+
type="button"
102+
>
103+
•••• •••• •••• ••••
104+
</button>
105+
<CardBrandLogo brand={brand} />
106+
</div>
107+
</>
108+
)}
109+
</div>
110+
</div>
111+
</div>
112+
);
113+
}
114+
115+
function CardBrandLogo({ brand }: { brand: string }) {
116+
if (brand === "visa") {
117+
return (
118+
<div className="flex h-6 w-10 items-center justify-center rounded bg-white/10 font-bold text-white text-xs italic">
119+
VISA
120+
</div>
121+
);
122+
}
123+
if (brand === "mastercard") {
124+
return (
125+
<div className="flex h-6 w-10 items-center justify-center">
126+
<div className="relative flex">
127+
<div className="h-5 w-5 rounded-full bg-red-500/90" />
128+
<div className="-ml-2 h-5 w-5 rounded-full bg-yellow-500/90" />
129+
</div>
130+
</div>
131+
);
132+
}
133+
if (brand === "amex") {
134+
return (
135+
<div className="flex h-6 w-10 items-center justify-center rounded bg-white/10 font-bold text-[8px] text-white">
136+
AMEX
137+
</div>
138+
);
139+
}
140+
return (
141+
<div className="flex h-6 w-10 items-center justify-center rounded bg-white/10">
142+
<CreditCardIcon className="text-white/80" size={16} weight="duotone" />
143+
</div>
144+
);
145+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"use client";
2+
3+
import {
4+
ArrowClockwiseIcon,
5+
TrendUpIcon,
6+
WarningCircleIcon,
7+
} from "@phosphor-icons/react";
8+
import { Button } from "@/components/ui/button";
9+
10+
/**
11+
* Empty state for the usage section when no features have been used yet.
12+
* Used in the main content area of billing overview.
13+
*/
14+
export function EmptyUsageState() {
15+
return (
16+
<div className="flex flex-col items-center justify-center py-16 text-center">
17+
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-muted">
18+
<TrendUpIcon
19+
className="text-muted-foreground"
20+
size={24}
21+
weight="duotone"
22+
/>
23+
</div>
24+
<p className="font-semibold">No usage data yet</p>
25+
<p className="mt-1 max-w-xs text-muted-foreground text-sm">
26+
Start using features to see your consumption stats here
27+
</p>
28+
</div>
29+
);
30+
}
31+
32+
type ErrorStateProps = {
33+
error: Error | unknown;
34+
onRetry: () => void;
35+
};
36+
37+
/**
38+
* Error state shown when billing data fails to load.
39+
* Provides a retry action for the user.
40+
*/
41+
export function ErrorState({ error, onRetry }: ErrorStateProps) {
42+
const errorMessage =
43+
error instanceof Error ? error.message : "Failed to load billing data";
44+
45+
return (
46+
<div className="flex h-full flex-col items-center justify-center p-8">
47+
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-destructive/10">
48+
<WarningCircleIcon
49+
className="text-destructive"
50+
size={24}
51+
weight="duotone"
52+
/>
53+
</div>
54+
<p className="font-semibold">Something went wrong</p>
55+
<p className="mt-1 mb-4 max-w-xs text-center text-muted-foreground text-sm">
56+
{errorMessage}
57+
</p>
58+
<Button onClick={onRetry} size="sm" variant="outline">
59+
<ArrowClockwiseIcon className="mr-2" size={14} />
60+
Try again
61+
</Button>
62+
</div>
63+
);
64+
}

0 commit comments

Comments
 (0)