From 2654f10ac18b286ffa0a967cc829750249bc2541 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 15 Feb 2026 15:59:57 +0300 Subject: [PATCH 1/6] style(my-usage): use Badge for provider group values Co-Authored-By: Claude Opus 4.6 --- .../_components/provider-group-info.tsx | 129 +++++++++++++++--- 1 file changed, 113 insertions(+), 16 deletions(-) diff --git a/src/app/[locale]/my-usage/_components/provider-group-info.tsx b/src/app/[locale]/my-usage/_components/provider-group-info.tsx index fab11b69b..0f2fec1da 100644 --- a/src/app/[locale]/my-usage/_components/provider-group-info.tsx +++ b/src/app/[locale]/my-usage/_components/provider-group-info.tsx @@ -2,8 +2,65 @@ import { Layers, ShieldCheck } from "lucide-react"; import { useTranslations } from "next-intl"; +import { Badge } from "@/components/ui/badge"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; +function abbreviateModel(name: string): string { + const parts = name.split("-"); + + if (parts.length === 1) { + return parts[0].length <= 4 ? parts[0].toUpperCase() : parts[0].slice(0, 2).toUpperCase(); + } + + const letterParts: string[] = []; + let versionMixed = ""; + const versionNums: string[] = []; + + for (const part of parts) { + if (/^\d{8,}$/.test(part)) continue; + if (/^[a-zA-Z]+$/.test(part)) { + letterParts.push(part); + } else if (/^\d+\.\d+$/.test(part)) { + versionMixed = part; + } else if (/^\d+[a-zA-Z]/.test(part)) { + versionMixed = part; + } else if (/^\d+$/.test(part)) { + versionNums.push(part); + } else { + letterParts.push(part); + } + } + + const prefix = letterParts + .slice(0, 3) + .map((w) => w[0].toUpperCase()) + .join(""); + + let version = ""; + if (versionMixed) { + version = versionMixed; + } else if (versionNums.length > 0) { + version = versionNums.slice(0, 2).join("."); + } + + if (version && prefix) { + return `${prefix}-${version}`; + } + return prefix || name.toUpperCase().substring(0, 3); +} + +function abbreviateClient(name: string): string { + const parts = name.split(/[-\s]+/); + if (parts.length === 1) { + return name.slice(0, 2).toUpperCase(); + } + return parts + .slice(0, 3) + .map((w) => w[0].toUpperCase()) + .join(""); +} + interface ProviderGroupInfoProps { keyProviderGroup: string | null; userProviderGroup: string | null; @@ -26,10 +83,8 @@ export function ProviderGroupInfo({ const userDisplay = userProviderGroup ?? tGroup("allProviders"); const inherited = !keyProviderGroup && !!userProviderGroup; - const modelsDisplay = - userAllowedModels.length > 0 ? userAllowedModels.join(", ") : tRestrictions("noRestrictions"); - const clientsDisplay = - userAllowedClients.length > 0 ? userAllowedClients.join(", ") : tRestrictions("noRestrictions"); + const hasModels = userAllowedModels.length > 0; + const hasClients = userAllowedClients.length > 0; return (
{tGroup("title")}
-
- {tGroup("keyGroup")}: - {keyDisplay} +
+ {tGroup("keyGroup")}: + + {keyDisplay} + {inherited && ( ({tGroup("inheritedFromUser")}) )}
-
- {tGroup("userGroup")}: - {userDisplay} +
+ {tGroup("userGroup")}: + + {userDisplay} +
@@ -66,13 +125,51 @@ export function ProviderGroupInfo({ {tRestrictions("title")}
-
- {tRestrictions("models")}: - {modelsDisplay} +
+ + {tRestrictions("models")}: + + {hasModels ? ( + userAllowedModels.map((name) => ( + + + + + {abbreviateModel(name)} + + + + {name} + + )) + ) : ( + + {tRestrictions("noRestrictions")} + + )}
-
- {tRestrictions("clients")}: - {clientsDisplay} +
+ + {tRestrictions("clients")}: + + {hasClients ? ( + userAllowedClients.map((name) => ( + + + + + {abbreviateClient(name)} + + + + {name} + + )) + ) : ( + + {tRestrictions("noRestrictions")} + + )}
From b8dc8caf3ee72606ee6fc9fc37315fae872ece5a Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 15 Feb 2026 16:28:19 +0300 Subject: [PATCH 2/6] fix(my-usage): use currency symbol instead of code in quota cards Replace manual `${currency} ${num.toFixed(2)}` formatting with `formatCurrency()` so quota values display "$3.50" instead of "USD 3.50", consistent with all other currency displays in the app. Co-Authored-By: Claude Opus 4.6 --- .../my-usage/_components/quota-cards.tsx | 193 +++++++++--------- 1 file changed, 95 insertions(+), 98 deletions(-) diff --git a/src/app/[locale]/my-usage/_components/quota-cards.tsx b/src/app/[locale]/my-usage/_components/quota-cards.tsx index 68a8a3a30..37af9c866 100644 --- a/src/app/[locale]/my-usage/_components/quota-cards.tsx +++ b/src/app/[locale]/my-usage/_components/quota-cards.tsx @@ -3,11 +3,11 @@ import { useTranslations } from "next-intl"; import { useMemo } from "react"; import type { MyUsageQuota } from "@/actions/my-usage"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Skeleton } from "@/components/ui/skeleton"; import type { CurrencyCode } from "@/lib/utils"; import { cn } from "@/lib/utils"; +import { formatCurrency } from "@/lib/utils/currency"; import { calculateUsagePercent, isUnlimited } from "@/lib/utils/limit-helpers"; interface QuotaCardsProps { @@ -79,144 +79,141 @@ export function QuotaCards({ quota, loading = false, currencyCode = "USD" }: Quo } return ( -
-
- {items.map((item) => { - const keyPct = calculateUsagePercent(item.keyCurrent, item.keyLimit); - const userPct = calculateUsagePercent(item.userCurrent ?? 0, item.userLimit); - - const keyTone = getTone(keyPct); - const userTone = getTone(userPct); - const hasUserData = item.userLimit !== null || item.userCurrent !== null; - - return ( - - - - {item.title} - - - -
- - -
-
-
- ); - })} - {items.length === 0 && !loading ? ( - - - {t("empty")} - - - ) : null} -
+
+ {items.map((item) => { + const isCurrency = item.key !== "concurrent"; + const currency = isCurrency ? currencyCode : undefined; + + return ( + + ); + })} + {items.length === 0 && !loading ? ( +
+ {t("empty")} +
+ ) : null}
); } -function QuotaCardsSkeleton({ label }: { label: string }) { +function QuotaBlock({ + title, + keyCurrent, + keyLimit, + userCurrent, + userLimit, + currency, +}: { + title: string; + keyCurrent: number; + keyLimit: number | null; + userCurrent: number; + userLimit: number | null; + currency?: CurrencyCode; +}) { + const t = useTranslations("myUsage.quota"); + + const keyPct = calculateUsagePercent(keyCurrent, keyLimit); + const userPct = calculateUsagePercent(userCurrent, userLimit); + return ( -
-
- {Array.from({ length: 6 }).map((_, index) => ( - - - - - -
- - -
-
-
- ))} -
-
- - {label} -
+
+
{title}
+ +
); } -function QuotaColumn({ +function QuotaRow({ label, current, limit, percent, - tone, currency, - muted = false, }: { label: string; current: number; limit: number | null; percent: number | null; - tone: "default" | "warn" | "danger"; - currency?: string; - muted?: boolean; + currency?: CurrencyCode; }) { const t = useTranslations("myUsage.quota"); + const unlimited = isUnlimited(limit); + const tone = getTone(percent); const formatValue = (value: number) => { const num = Number(value); - if (!Number.isFinite(num)) { - return currency ? `${currency} 0.00` : "0"; - } - return currency ? `${currency} ${num.toFixed(2)}` : String(num); + if (!Number.isFinite(num)) return currency ? formatCurrency(0, currency) : "0"; + return currency ? formatCurrency(num, currency) : String(num); }; - const unlimited = isUnlimited(limit); + const limitDisplay = unlimited ? t("unlimited") : formatValue(limit as number); + const ariaLabel = `${label}: ${formatValue(current)}${!unlimited ? ` / ${limitDisplay}` : ""}`; - const progressClass = cn("h-2", { + const progressClass = cn("h-1.5 flex-1", { "bg-destructive/10 [&>div]:bg-destructive": tone === "danger", "bg-amber-500/10 [&>div]:bg-amber-500": tone === "warn", }); - const limitDisplay = unlimited ? t("unlimited") : formatValue(limit as number); - const ariaLabel = `${label}: ${formatValue(current)}${!unlimited ? ` / ${limitDisplay}` : ""}`; - return ( -
- {/* Label */} -
{label}
- - {/* Values - split into two lines to avoid overlap */} -
-
{formatValue(current)}
-
/ {limitDisplay}
-
- - {/* Progress bar or placeholder */} +
+ {label} {!unlimited ? ( ) : (
)} + + {formatValue(current)} + / {limitDisplay} + +
+ ); +} + +function QuotaCardsSkeleton({ label }: { label: string }) { + return ( +
+
+ {Array.from({ length: 6 }).map((_, index) => ( +
+ + + +
+ ))} +
+
+ + {label} +
); } From 91a571e92240ca26b0b620702289208535ceff5f Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 15 Feb 2026 16:45:22 +0300 Subject: [PATCH 3/6] style(my-usage): replace unlimited text with infinity icon in quota cards Co-Authored-By: Claude Opus 4.6 --- src/app/[locale]/my-usage/_components/quota-cards.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/[locale]/my-usage/_components/quota-cards.tsx b/src/app/[locale]/my-usage/_components/quota-cards.tsx index 37af9c866..fd44b5580 100644 --- a/src/app/[locale]/my-usage/_components/quota-cards.tsx +++ b/src/app/[locale]/my-usage/_components/quota-cards.tsx @@ -1,5 +1,6 @@ "use client"; +import { Infinity } from "lucide-react"; import { useTranslations } from "next-intl"; import { useMemo } from "react"; import type { MyUsageQuota } from "@/actions/my-usage"; @@ -192,7 +193,10 @@ function QuotaRow({ )} {formatValue(current)} - / {limitDisplay} + + {" / "} + {unlimited ? : limitDisplay} +
); From fe1fe4efdbc59650d7ee1d699325c3aca9861230 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 15 Feb 2026 16:47:01 +0300 Subject: [PATCH 4/6] fix(my-usage): paginate model breakdown in statistics summary card Co-Authored-By: Claude Opus 4.6 --- .../_components/statistics-summary-card.tsx | 168 ++++++++++++------ 1 file changed, 115 insertions(+), 53 deletions(-) diff --git a/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx index 1d0052018..94733d2fe 100644 --- a/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx +++ b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx @@ -6,6 +6,8 @@ import { ArrowDownRight, ArrowUpRight, BarChart3, + ChevronLeft, + ChevronRight, Coins, Database, Hash, @@ -15,7 +17,11 @@ import { } from "lucide-react"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useRef, useState } from "react"; -import { getMyStatsSummary, type MyStatsSummary } from "@/actions/my-usage"; +import { + getMyStatsSummary, + type ModelBreakdownItem, + type MyStatsSummary, +} from "@/actions/my-usage"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; @@ -110,9 +116,26 @@ export function StatisticsSummaryCard({ setDateRange(range); }, []); + const [breakdownPage, setBreakdownPage] = useState(1); + + // Reset breakdown page when stats change (date range switch, refresh) + useEffect(() => { + setBreakdownPage(1); + }, [stats]); + const isLoading = loading || refreshing; const currencyCode = stats?.currencyCode ?? "USD"; + const maxBreakdownLen = Math.max( + stats?.keyModelBreakdown.length ?? 0, + stats?.userModelBreakdown.length ?? 0 + ); + const breakdownTotalPages = Math.ceil(maxBreakdownLen / MODEL_BREAKDOWN_PAGE_SIZE); + const sliceStart = (breakdownPage - 1) * MODEL_BREAKDOWN_PAGE_SIZE; + const sliceEnd = breakdownPage * MODEL_BREAKDOWN_PAGE_SIZE; + const keyPageItems = stats?.keyModelBreakdown.slice(sliceStart, sliceEnd) ?? []; + const userPageItems = stats?.userModelBreakdown.slice(sliceStart, sliceEnd) ?? []; + return ( @@ -220,60 +243,62 @@ export function StatisticsSummaryCard({

{t("modelBreakdown")}

- {/* Key Stats */} -
-

- {t("keyStats")} -

- {stats.keyModelBreakdown.length > 0 ? ( -
- {stats.keyModelBreakdown.map((item, index) => ( - - ))} -
- ) : ( -

{t("noData")}

- )} -
+ {keyPageItems.length > 0 && ( +
+

+ {t("keyStats")} +

+ +
+ )} + + {userPageItems.length > 0 && ( +
+

+ {t("userStats")} +

+ +
+ )} +
- {/* User Stats */} -
-

- {t("userStats")} -

- {stats.userModelBreakdown.length > 0 ? ( -
- {stats.userModelBreakdown.map((item, index) => ( - - ))} -
- ) : ( -

{t("noData")}

- )} + {breakdownTotalPages > 1 && ( +
+ + + {breakdownPage} / {breakdownTotalPages} + +
-
+ )}
) : ( @@ -284,6 +309,43 @@ export function StatisticsSummaryCard({ ); } +const MODEL_BREAKDOWN_PAGE_SIZE = 5; + +interface ModelBreakdownColumnProps { + pageItems: ModelBreakdownItem[]; + currencyCode: CurrencyCode; + totalCost: number; + keyPrefix: string; + pageOffset: number; +} + +function ModelBreakdownColumn({ + pageItems, + currencyCode, + totalCost, + keyPrefix, + pageOffset, +}: ModelBreakdownColumnProps) { + return ( +
+ {pageItems.map((item, index) => ( + + ))} +
+ ); +} + interface ModelBreakdownRowProps { model: string | null; requests: number; From 1576a25f3298dba8f63e05be169c96d3f7e2c3c6 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 15 Feb 2026 16:54:57 +0300 Subject: [PATCH 5/6] chore(my-usage): suppress biome exhaustive-deps for intentional stats reset Co-Authored-By: Claude Opus 4.6 --- .../[locale]/my-usage/_components/statistics-summary-card.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx index 94733d2fe..df0196243 100644 --- a/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx +++ b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx @@ -119,6 +119,7 @@ export function StatisticsSummaryCard({ const [breakdownPage, setBreakdownPage] = useState(1); // Reset breakdown page when stats change (date range switch, refresh) + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional reset on stats identity change useEffect(() => { setBreakdownPage(1); }, [stats]); From 3007012db2f5a800354a0da3c9e1eae94e293db7 Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 15 Feb 2026 17:10:51 +0300 Subject: [PATCH 6/6] fix(my-usage): address PR #794 review comments - Fix abbreviateModel/abbreviateClient crash on empty split parts - Fix pagination reset on auto-refresh by using dateRange deps - Restore noData fallback in model breakdown columns - Add i18n for pagination controls with aria-labels (5 langs) - Fix quota label overflow for long translations (w-8 -> w-auto) - Rename Infinity -> InfinityIcon to avoid shadowing global - Remove redundant span wrappers in TooltipTrigger asChild Co-Authored-By: Claude Opus 4.6 --- messages/en/myUsage.json | 3 ++ messages/ja/myUsage.json | 3 ++ messages/ru/myUsage.json | 3 ++ messages/zh-CN/myUsage.json | 3 ++ messages/zh-TW/myUsage.json | 3 ++ .../_components/collapsible-quota-card.tsx | 8 ++-- .../_components/provider-group-info.tsx | 20 ++++----- .../my-usage/_components/quota-cards.tsx | 8 ++-- .../_components/statistics-summary-card.tsx | 45 +++++++++++-------- 9 files changed, 59 insertions(+), 37 deletions(-) diff --git a/messages/en/myUsage.json b/messages/en/myUsage.json index 0ebe076a2..0e39e8496 100644 --- a/messages/en/myUsage.json +++ b/messages/en/myUsage.json @@ -92,6 +92,9 @@ "keyStats": "Key", "userStats": "User", "noData": "No data for selected period", + "breakdownPrevPage": "Previous page", + "breakdownNextPage": "Next page", + "breakdownPageIndicator": "{current} / {total}", "unknownModel": "Unknown", "modal": { "requests": "Requests", diff --git a/messages/ja/myUsage.json b/messages/ja/myUsage.json index 4d0b1bb7e..901e10ab6 100644 --- a/messages/ja/myUsage.json +++ b/messages/ja/myUsage.json @@ -92,6 +92,9 @@ "keyStats": "キー", "userStats": "ユーザー", "noData": "選択期間のデータがありません", + "breakdownPrevPage": "前のページ", + "breakdownNextPage": "次のページ", + "breakdownPageIndicator": "{current} / {total}", "unknownModel": "不明", "modal": { "requests": "リクエスト", diff --git a/messages/ru/myUsage.json b/messages/ru/myUsage.json index bb3b61bd7..5ccfec871 100644 --- a/messages/ru/myUsage.json +++ b/messages/ru/myUsage.json @@ -92,6 +92,9 @@ "keyStats": "Ключ", "userStats": "Пользователь", "noData": "Нет данных за выбранный период", + "breakdownPrevPage": "Предыдущая страница", + "breakdownNextPage": "Следующая страница", + "breakdownPageIndicator": "{current} / {total}", "unknownModel": "Неизвестно", "modal": { "requests": "Запросов", diff --git a/messages/zh-CN/myUsage.json b/messages/zh-CN/myUsage.json index 9eaaf7925..6cf939337 100644 --- a/messages/zh-CN/myUsage.json +++ b/messages/zh-CN/myUsage.json @@ -92,6 +92,9 @@ "keyStats": "密钥", "userStats": "用户", "noData": "所选时段无数据", + "breakdownPrevPage": "上一页", + "breakdownNextPage": "下一页", + "breakdownPageIndicator": "{current} / {total}", "unknownModel": "未知", "modal": { "requests": "请求", diff --git a/messages/zh-TW/myUsage.json b/messages/zh-TW/myUsage.json index b5247a160..f803a617b 100644 --- a/messages/zh-TW/myUsage.json +++ b/messages/zh-TW/myUsage.json @@ -92,6 +92,9 @@ "keyStats": "金鑰", "userStats": "使用者", "noData": "所選時段無資料", + "breakdownPrevPage": "上一頁", + "breakdownNextPage": "下一頁", + "breakdownPageIndicator": "{current} / {total}", "unknownModel": "不明", "modal": { "requests": "請求", diff --git a/src/app/[locale]/my-usage/_components/collapsible-quota-card.tsx b/src/app/[locale]/my-usage/_components/collapsible-quota-card.tsx index ee7e0db50..4d172d1fd 100644 --- a/src/app/[locale]/my-usage/_components/collapsible-quota-card.tsx +++ b/src/app/[locale]/my-usage/_components/collapsible-quota-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { AlertTriangle, ChevronDown, Infinity, PieChart } from "lucide-react"; +import { AlertTriangle, ChevronDown, Infinity as InfinityIcon, PieChart } from "lucide-react"; import { useTranslations } from "next-intl"; import { useState } from "react"; import type { MyUsageQuota } from "@/actions/my-usage"; @@ -94,7 +94,7 @@ export function CollapsibleQuotaCard({
{t("daily")}: {dailyPct === null ? ( - + ) : ( <> @@ -108,7 +108,7 @@ export function CollapsibleQuotaCard({
{t("monthly")}: {monthlyPct === null ? ( - + ) : ( <> @@ -122,7 +122,7 @@ export function CollapsibleQuotaCard({
{t("total")}: {totalPct === null ? ( - + ) : ( <> diff --git a/src/app/[locale]/my-usage/_components/provider-group-info.tsx b/src/app/[locale]/my-usage/_components/provider-group-info.tsx index 0f2fec1da..227087f1b 100644 --- a/src/app/[locale]/my-usage/_components/provider-group-info.tsx +++ b/src/app/[locale]/my-usage/_components/provider-group-info.tsx @@ -7,7 +7,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip import { cn } from "@/lib/utils"; function abbreviateModel(name: string): string { - const parts = name.split("-"); + const parts = name.split("-").filter(Boolean); if (parts.length === 1) { return parts[0].length <= 4 ? parts[0].toUpperCase() : parts[0].slice(0, 2).toUpperCase(); @@ -51,7 +51,7 @@ function abbreviateModel(name: string): string { } function abbreviateClient(name: string): string { - const parts = name.split(/[-\s]+/); + const parts = name.split(/[-\s]+/).filter(Boolean); if (parts.length === 1) { return name.slice(0, 2).toUpperCase(); } @@ -133,11 +133,9 @@ export function ProviderGroupInfo({ userAllowedModels.map((name) => ( - - - {abbreviateModel(name)} - - + + {abbreviateModel(name)} + {name} @@ -156,11 +154,9 @@ export function ProviderGroupInfo({ userAllowedClients.map((name) => ( - - - {abbreviateClient(name)} - - + + {abbreviateClient(name)} + {name} diff --git a/src/app/[locale]/my-usage/_components/quota-cards.tsx b/src/app/[locale]/my-usage/_components/quota-cards.tsx index fd44b5580..9f7496fc9 100644 --- a/src/app/[locale]/my-usage/_components/quota-cards.tsx +++ b/src/app/[locale]/my-usage/_components/quota-cards.tsx @@ -1,6 +1,6 @@ "use client"; -import { Infinity } from "lucide-react"; +import { Infinity as InfinityIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useMemo } from "react"; import type { MyUsageQuota } from "@/actions/my-usage"; @@ -180,7 +180,9 @@ function QuotaRow({ return (
- {label} + + {label} + {!unlimited ? ( ) : ( @@ -195,7 +197,7 @@ function QuotaRow({ {formatValue(current)} {" / "} - {unlimited ? : limitDisplay} + {unlimited ? : limitDisplay}
diff --git a/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx index df0196243..a73947cea 100644 --- a/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx +++ b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx @@ -118,11 +118,11 @@ export function StatisticsSummaryCard({ const [breakdownPage, setBreakdownPage] = useState(1); - // Reset breakdown page when stats change (date range switch, refresh) - // biome-ignore lint/correctness/useExhaustiveDependencies: intentional reset on stats identity change + // Reset breakdown page when date range changes + // biome-ignore lint/correctness/useExhaustiveDependencies: deps used as reset trigger on date range change useEffect(() => { setBreakdownPage(1); - }, [stats]); + }, [dateRange.startDate, dateRange.endDate]); const isLoading = loading || refreshing; const currencyCode = stats?.currencyCode ?? "USD"; @@ -244,11 +244,11 @@ export function StatisticsSummaryCard({

{t("modelBreakdown")}

- {keyPageItems.length > 0 && ( -
-

- {t("keyStats")} -

+
+

+ {t("keyStats")} +

+ {keyPageItems.length > 0 ? ( -
- )} + ) : ( +

{t("noData")}

+ )} +
- {userPageItems.length > 0 && ( -
-

- {t("userStats")} -

+
+

+ {t("userStats")} +

+ {userPageItems.length > 0 ? ( -
- )} + ) : ( +

{t("noData")}

+ )} +
{breakdownTotalPages > 1 && ( @@ -281,18 +285,23 @@ export function StatisticsSummaryCard({ size="icon" variant="ghost" className="h-7 w-7" + aria-label={t("breakdownPrevPage")} disabled={breakdownPage <= 1} onClick={() => setBreakdownPage((p) => p - 1)} > - {breakdownPage} / {breakdownTotalPages} + {t("breakdownPageIndicator", { + current: breakdownPage, + total: breakdownTotalPages, + })}