2026-06-12 02:47:55 +02:00
|
|
|
import { useState } from "react";
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
import { useTranslation } from "react-i18next";
|
2026-06-12 02:47:55 +02:00
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
|
|
|
import { api, type AdminQuotaRow } from "../lib/api";
|
|
|
|
|
import { quotaActionLabel } from "../lib/format";
|
|
|
|
|
|
|
|
|
|
const RANGES = [7, 30, 90] as const;
|
|
|
|
|
|
|
|
|
|
export default function Stats() {
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
const { t } = useTranslation();
|
2026-06-12 02:47:55 +02:00
|
|
|
const [days, setDays] = useState<number>(30);
|
|
|
|
|
const q = useQuery({
|
|
|
|
|
queryKey: ["admin-quota", days],
|
|
|
|
|
queryFn: () => api.adminQuota(days),
|
|
|
|
|
});
|
|
|
|
|
const data = q.data;
|
|
|
|
|
const maxDaily = Math.max(1, ...(data?.daily ?? []).map((d) => d.total));
|
|
|
|
|
const grandTotal = (data?.rows ?? []).reduce((s, r) => s + r.total, 0);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-4 max-w-4xl mx-auto">
|
|
|
|
|
<div className="flex items-center justify-between gap-3 mb-4">
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<h2 className="text-lg font-semibold">{t("stats.title")}</h2>
|
2026-06-12 02:47:55 +02:00
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
{RANGES.map((d) => (
|
|
|
|
|
<button
|
|
|
|
|
key={d}
|
|
|
|
|
onClick={() => setDays(d)}
|
|
|
|
|
className={`px-3 py-1.5 rounded-full text-sm border transition ${
|
|
|
|
|
days === d
|
|
|
|
|
? "bg-accent text-accent-fg border-accent"
|
|
|
|
|
: "bg-card border-border text-muted hover:border-accent"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
{t("stats.rangeDays", { count: d })}
|
2026-06-12 02:47:55 +02:00
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p className="text-xs text-muted mb-4 leading-relaxed">
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
{t("stats.introBefore")}
|
|
|
|
|
<b className="text-fg/80">{t("stats.introSystem")}</b>
|
|
|
|
|
{t("stats.introAfter")}
|
2026-06-12 02:47:55 +02:00
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{q.isLoading ? (
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<div className="text-muted py-8">{t("stats.loading")}</div>
|
2026-06-12 02:47:55 +02:00
|
|
|
) : !data ? (
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<div className="text-muted py-8">{t("stats.noData")}</div>
|
2026-06-12 02:47:55 +02:00
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{/* Daily totals (instance-wide, Pacific days) */}
|
|
|
|
|
<div className="glass-card rounded-xl p-3 mb-4">
|
|
|
|
|
<div className="text-xs text-muted mb-2">
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
{t("stats.dailyTotal", {
|
|
|
|
|
count: data.range_days,
|
|
|
|
|
units: grandTotal.toLocaleString(),
|
|
|
|
|
})}
|
2026-06-12 02:47:55 +02:00
|
|
|
</div>
|
|
|
|
|
{data.daily.length === 0 ? (
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<div className="text-muted text-sm">{t("stats.noUsageInRange")}</div>
|
2026-06-12 02:47:55 +02:00
|
|
|
) : (
|
|
|
|
|
<div className="flex items-end gap-0.5 h-24">
|
|
|
|
|
{data.daily.map((d) => (
|
2026-06-17 13:55:43 +02:00
|
|
|
// Each day gets a full-height track so the column is always visible
|
|
|
|
|
// (even on near-zero days); the accent fill renders the value on top.
|
2026-06-12 02:47:55 +02:00
|
|
|
<div
|
|
|
|
|
key={d.day}
|
2026-06-17 13:55:43 +02:00
|
|
|
className="flex-1 h-full flex items-end bg-card/50 rounded-t"
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
title={t("stats.dailyTooltip", {
|
|
|
|
|
day: d.day,
|
|
|
|
|
units: d.total.toLocaleString(),
|
|
|
|
|
})}
|
2026-06-17 13:55:43 +02:00
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="w-full bg-accent hover:opacity-80 rounded-t transition"
|
|
|
|
|
style={{ height: `${Math.max(3, (d.total / maxDaily) * 100)}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-06-12 02:47:55 +02:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Per-user breakdown */}
|
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
|
|
|
{data.rows.length === 0 ? (
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
<div className="text-muted py-4">{t("stats.noUsageInRange")}</div>
|
2026-06-12 02:47:55 +02:00
|
|
|
) : (
|
|
|
|
|
data.rows.map((r) => (
|
|
|
|
|
<UserRow key={r.user_id ?? "system"} row={r} max={data.rows[0]?.total || 1} />
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function UserRow({ row, max }: { row: AdminQuotaRow; max: number }) {
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
const { t } = useTranslation();
|
2026-06-12 02:47:55 +02:00
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const actions = Object.entries(row.by_action).sort((a, b) => b[1] - a[1]);
|
|
|
|
|
const isSystem = row.user_id === null;
|
|
|
|
|
return (
|
|
|
|
|
<div className="glass-card rounded-xl p-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpen((o) => !o)}
|
|
|
|
|
className="w-full flex items-center gap-3 text-left"
|
|
|
|
|
>
|
|
|
|
|
<span className={`text-sm font-medium truncate flex-1 ${isSystem ? "text-muted" : ""}`}>
|
feat(i18n): translate remaining components (HU/EN/DE)
Feed, VideoCard, Sidebar, PlayerModal, Channels, Stats, SettingsPanel,
OnboardingWizard, NotificationCenter, Toaster, ErrorBoundary and the relativeTime
helper are now fully translated in Hungarian, English and German, each with its own
locale area file (auto-loaded). Key parity verified across all three languages.
2026-06-15 00:47:04 +02:00
|
|
|
{isSystem ? t("stats.system") : row.email}
|
2026-06-12 02:47:55 +02:00
|
|
|
</span>
|
|
|
|
|
<span className="text-sm font-semibold tabular-nums shrink-0">
|
|
|
|
|
{row.total.toLocaleString()}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
{/* Proportion bar */}
|
|
|
|
|
<div className="mt-2 h-1.5 bg-border/40 rounded-full overflow-hidden">
|
|
|
|
|
<div
|
|
|
|
|
className="h-full bg-accent rounded-full"
|
|
|
|
|
style={{ width: `${Math.max(2, (row.total / max) * 100)}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{open && actions.length > 0 && (
|
|
|
|
|
<div className="mt-2 space-y-1 text-xs text-muted">
|
|
|
|
|
{actions.map(([action, units]) => (
|
|
|
|
|
<div key={action} className="flex items-center justify-between gap-2">
|
|
|
|
|
<span>{quotaActionLabel(action)}</span>
|
|
|
|
|
<span className="tabular-nums">{units.toLocaleString()}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|