feat(web): add subscription summary card to billing page
Billing was payment history only. Add a summary card at the top showing the current plan, status badge, and next renewal / trial-end / end date (from /subscriptions/me), with a "Manage subscription" link — or a "Choose a plan" CTA when there's no active subscription. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -385,6 +385,21 @@
|
||||
"methodLabels": {
|
||||
"stripe": "Credit Card",
|
||||
"eft": "EFT/Wire"
|
||||
},
|
||||
"summary": {
|
||||
"title": "Current Plan",
|
||||
"noPlan": "You have no active subscription.",
|
||||
"managePlan": "Manage subscription",
|
||||
"choosePlan": "Choose a plan",
|
||||
"nextRenewal": "Next renewal",
|
||||
"trialEnds": "Trial ends",
|
||||
"ends": "Ends on"
|
||||
},
|
||||
"subStatus": {
|
||||
"trial": "Trial",
|
||||
"active": "Active",
|
||||
"canceled": "Canceled",
|
||||
"expired": "Expired"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -385,6 +385,21 @@
|
||||
"methodLabels": {
|
||||
"stripe": "Kredi Kartı",
|
||||
"eft": "EFT/Havale"
|
||||
},
|
||||
"summary": {
|
||||
"title": "Mevcut Plan",
|
||||
"noPlan": "Aktif aboneliğiniz yok.",
|
||||
"managePlan": "Aboneliği Yönet",
|
||||
"choosePlan": "Plan Seç",
|
||||
"nextRenewal": "Sonraki yenileme",
|
||||
"trialEnds": "Deneme bitişi",
|
||||
"ends": "Bitiş tarihi"
|
||||
},
|
||||
"subStatus": {
|
||||
"trial": "Deneme",
|
||||
"active": "Aktif",
|
||||
"canceled": "İptal edildi",
|
||||
"expired": "Süresi doldu"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -7,14 +7,22 @@ import { Button } from "@sase/ui";
|
||||
import { Skeleton } from "@sase/ui";
|
||||
import { Separator } from "@sase/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Download, Filter } from "lucide-react";
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
import { ArrowRight, Download, Filter } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/dashboard/billing")({
|
||||
component: BillingPage,
|
||||
});
|
||||
|
||||
interface Subscription {
|
||||
status: "trial" | "active" | "canceled" | "expired" | string;
|
||||
plan?: { name: string; key: string };
|
||||
billingPeriod?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
interface Payment {
|
||||
id: string;
|
||||
amount: number;
|
||||
@@ -41,6 +49,31 @@ function BillingPage() {
|
||||
queryFn: () => api.get<Payment[]>("/payments/me"),
|
||||
});
|
||||
|
||||
const { data: subData, isLoading: subLoading } = useQuery({
|
||||
queryKey: ["subscription", "me"],
|
||||
queryFn: () =>
|
||||
api.get<{ subscription: Subscription | null; eligibleForTrial: boolean }>(
|
||||
"/subscriptions/me",
|
||||
),
|
||||
});
|
||||
const subscription = subData?.subscription;
|
||||
|
||||
const formatDate = (iso: string) =>
|
||||
new Date(iso).toLocaleDateString(locale === "tr" ? "tr-TR" : "en-US", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const subStatusVariant: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
||||
active: "default",
|
||||
trial: "secondary",
|
||||
canceled: "outline",
|
||||
expired: "destructive",
|
||||
};
|
||||
const endDateLabel =
|
||||
subscription?.status === "trial" ? t("billing.summary.trialEnds") : t("billing.summary.ends");
|
||||
|
||||
const statusVariants: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
||||
completed: "default",
|
||||
pending: "secondary",
|
||||
@@ -58,6 +91,45 @@ function BillingPage() {
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<h2 className="text-2xl font-bold">{t("billing.title")}</h2>
|
||||
|
||||
{/* Subscription summary */}
|
||||
{subLoading ? (
|
||||
<Skeleton className="h-24 w-full rounded-xl" />
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-4 py-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{t("billing.summary.title")}
|
||||
</p>
|
||||
{subscription ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-semibold">{subscription.plan?.name ?? "—"}</span>
|
||||
<Badge variant={subStatusVariant[subscription.status] ?? "secondary"}>
|
||||
{t(`billing.subStatus.${subscription.status}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
{subscription.endDate && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{endDateLabel}:{" "}
|
||||
<span className="tabular-nums">{formatDate(subscription.endDate)}</span>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">{t("billing.summary.noPlan")}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button asChild variant={subscription ? "outline" : "default"} size="sm">
|
||||
<Link to="/dashboard/subscription">
|
||||
{subscription ? t("billing.summary.managePlan") : t("billing.summary.choosePlan")}
|
||||
<ArrowRight className="ml-1.5 size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user