mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
* 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>
129 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|