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:
2026-05-27 00:04:58 +03:00
parent acada22e71
commit 36bd4b500b
3 changed files with 104 additions and 2 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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">