Files
OmniRoute/src/shared/components/EmptyState.tsx
nguyenha935 4012bac41d fix(i18n): preserve remaining Vietnamese localization (#7935)
* fix(i18n): preserve remaining Vietnamese localization

* chore(quality): rebaseline file-size cap for 9 dashboard components (i18n wiring)

Restoring the Vietnamese localization on 9 dashboard components (useTranslations
wiring + t()/tc() call-site swaps for previously hardcoded strings) grows each
file by a small, irreducible amount. Bumps the frozen file-size-baseline.json
caps to match, with a justification entry per the project's own ratchet policy.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(i18n): wire weekday localization + add missing qwen CLI description

Two gaps left by this PR's own new contract tests, caught while
reconciling the branch against the release tip:

- CostOverviewTab.tsx added formatWeekdayLabel() but never called it;
  the Weekly Usage Pattern chart still showed raw English day
  abbreviations regardless of locale. Now maps weeklyPattern rows
  through it before handing them to WeeklyPatternCard.
- cliTools.toolDescriptions was missing an entry for "qwen" (a
  baseUrlSupport:"full" tool) in both en.json and vi.json, failing
  the PR's own cli-catalog-display-contract.test.ts.

Covered by the PR's existing tests/unit/dashboard-localization-contract.test.ts
and tests/unit/cli-catalog-display-contract.test.ts (both now pass).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* i18n(vi): backfill the 4 proxySubscription keys #7299 added to en.json

#7299 (proxy subscriptions) merged while this branch was rebasing, adding
settings.proxySubscriptionsTab and settings.proxySubscription.error.{LOCAL_CORE_ENDPOINT_INVALID,
NEEDS_CORE_NOT_CONFIGURED,NO_USABLE_NODES} to en.json. This PR's own
i18n-vi-completeness contract asserts full en↔vi key parity, so the merge of the
current release tip surfaced them as missing. Adds the Vietnamese translations,
keeping the SS/VMess/Trojan/VLESS/SOCKS5 technical terms verbatim.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
2026-07-21 13:41:02 -03:00

129 lines
3.3 KiB
TypeScript

"use client";
import { useTranslations } from "next-intl";
/**
* EmptyState — FASE-07 UX
*
* Reusable empty state component for dashboard sections when no data
* is available. Provides visual feedback and optional action button.
*
* Usage:
* <EmptyState
* icon="📡"
* title="No providers yet"
* description="Add your first API provider to get started."
* actionLabel="Add Provider"
* onAction={() => router.push('/providers/add')}
* />
*/
interface EmptyStateProps {
icon?: string;
title?: string;
description?: string;
actionLabel?: string;
onAction?: (() => void) | null;
}
export default function EmptyState({
icon = "📭",
title,
description = "",
actionLabel = "",
onAction = null,
}: EmptyStateProps) {
const t = useTranslations("common");
const resolvedTitle = title ?? t("nothingHere");
const usesMaterialSymbol = /^[a-z][a-z0-9_]*$/.test(icon);
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "48px 24px",
textAlign: "center",
minHeight: "200px",
}}
>
<div
style={{
fontSize: "48px",
marginBottom: "16px",
opacity: 0.8,
animation: "emptyBounce 2s ease-in-out infinite",
}}
role="img"
aria-hidden="true"
>
{usesMaterialSymbol ? (
<span className="material-symbols-outlined" style={{ fontSize: "inherit" }}>
{icon}
</span>
) : (
icon
)}
</div>
<h3
style={{
fontSize: "18px",
fontWeight: 600,
color: "var(--text-primary, #e0e0e0)",
marginBottom: "8px",
margin: 0,
}}
>
{resolvedTitle}
</h3>
{description && (
<p
style={{
fontSize: "14px",
color: "var(--text-secondary, #888)",
maxWidth: "320px",
lineHeight: 1.5,
marginTop: "8px",
}}
>
{description}
</p>
)}
{actionLabel && onAction && (
<button
onClick={onAction}
style={{
marginTop: "20px",
padding: "10px 24px",
borderRadius: "8px",
border: "1px solid rgba(99, 102, 241, 0.4)",
background: "rgba(99, 102, 241, 0.15)",
color: "#818cf8",
fontSize: "14px",
fontWeight: 500,
cursor: "pointer",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.background = "rgba(99, 102, 241, 0.25)";
(e.currentTarget as HTMLElement).style.transform = "translateY(-1px)";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.background = "rgba(99, 102, 241, 0.15)";
(e.currentTarget as HTMLElement).style.transform = "translateY(0)";
}}
>
{actionLabel}
</button>
)}
<style>{`
@keyframes emptyBounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
`}</style>
</div>
);
}