Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion admin-web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
<meta name="referrer" content="no-referrer" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>Buzz admin</title>
</head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700;800&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
Expand Down
37 changes: 30 additions & 7 deletions admin-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useState,
} from "react";
import { ApiFailure, request } from "./api";
import { getLang, setLang as persistLang, t, type Lang } from "./i18n";
import type {
FeedbackDetail,
FeedbackSummary,
Expand Down Expand Up @@ -68,16 +69,16 @@ function StateView<T>({
children: (data: T) => ReactNode;
}) {
if (resource.loading && !resource.data)
return <div className="state">Loading…</div>;
return <div className="state">{t("loading")}</div>;
if (resource.error && !resource.data) {
const forbidden =
resource.error instanceof ApiFailure && resource.error.status === 403;
return (
<div className="state error" role="alert">
<h2>{forbidden ? "Access denied" : "Could not load data"}</h2>
<h2>{forbidden ? t("access_denied") : t("load_failed")}</h2>
<p>{resource.error.message}</p>
<button type="button" onClick={resource.refetch}>
Retry
{t("retry")}
</button>
</div>
);
Expand Down Expand Up @@ -799,6 +800,7 @@ function ArrowIcon() {

export function App() {
const { path } = usePath();
const [lang, setLangState] = useState<Lang>(() => getLang());
const report = path.match(/^\/reports\/([^/]+)$/);
const feedback = path.match(/^\/feedback\/([^/]+)$/);
const content = report ? (
Expand All @@ -810,24 +812,45 @@ export function App() {
) : (
<Reports />
);
const switchLang = (next: Lang) => {
persistLang(next);
setLangState(next);
document.documentElement.lang = next === "zh" ? "zh-CN" : "en";
};
return (
<div className="app">
<div className="app" data-lang={lang}>
<header className="app-header">
<Link href="/reports" className="brand">
<span className="brand-mark">
<BuzzMark />
</span>
<span>
Buzz <b>Admin</b>
{t("brand", lang)}
</span>
</Link>
<nav>
<Link href="/reports" className="nav-link" activeWhenNested>
<ReportIcon /> Reports
<ReportIcon /> {t("nav_reports", lang)}
</Link>
<Link href="/feedback" className="nav-link" activeWhenNested>
<FeedbackIcon /> Feedback
<FeedbackIcon /> {t("nav_feedback", lang)}
</Link>
<span className="lang-switch" aria-label={t("lang", lang)}>
<button
type="button"
className={lang === "en" ? "on" : undefined}
onClick={() => switchLang("en")}
>
EN
</button>
<button
type="button"
className={lang === "zh" ? "on" : undefined}
onClick={() => switchLang("zh")}
>
中文
</button>
</span>
</nav>
</header>
<main>{content}</main>
Expand Down
41 changes: 41 additions & 0 deletions admin-web/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export type Lang = "en" | "zh";

const dict = {
en: {
brand: "Buzz Admin",
nav_reports: "Reports",
nav_feedback: "Feedback",
loading: "Loading…",
access_denied: "Access denied",
load_failed: "Could not load data",
retry: "Retry",
lang: "Language",
},
zh: {
brand: "Buzz 管理台",
nav_reports: "举报",
nav_feedback: "反馈",
loading: "加载中…",
access_denied: "无权限",
load_failed: "无法加载数据",
retry: "重试",
lang: "语言",
},
} as const;

export type MsgKey = keyof (typeof dict)["en"];

const STORAGE_KEY = "buzz-admin-lang";

export function getLang(): Lang {
const v = localStorage.getItem(STORAGE_KEY);
return v === "zh" || v === "en" ? v : "zh";
}

export function setLang(lang: Lang) {
localStorage.setItem(STORAGE_KEY, lang);
}

export function t(key: MsgKey, lang: Lang = getLang()): string {
return dict[lang][key] ?? dict.en[key] ?? key;
}
49 changes: 34 additions & 15 deletions admin-web/src/styles.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700;800&display=swap");

:root {
font-family:
"Helvetica Neue", Helvetica, Arial, ui-sans-serif, system-ui, sans-serif;
color: #231e1e;
background: #d7e7f6;
font-family: "Space Grotesk", "Segoe UI", "PingFang SC", "Helvetica Neue", sans-serif;
color: #1c1917;
background: #fff8f0;
font-synthesis: none;
letter-spacing: -0.01em;
}

* {
Expand All @@ -29,17 +31,18 @@ select {
}

button {
color: white;
background: #231e1e;
border: 0;
border-radius: 999px;
color: #fafaf9;
background: #1c1917;
border: 3px solid #1c1917;
border-radius: 0;
padding: 0.75rem 1.25rem;
cursor: pointer;
font-weight: 700;
}

.app {
min-height: 100vh;
background: linear-gradient(180deg, #d7d72e 0%, #dfe379 24%, #d7e7f6 78%);
background: #fff8f0;
}

.app-header {
Expand Down Expand Up @@ -72,8 +75,8 @@ button {
display: grid;
place-items: center;
border-radius: 0.7rem;
color: #d7d72e;
background: #231e1e;
color: #1c1917;
background: #fbbf24;
}

.brand-mark svg {
Expand All @@ -86,10 +89,9 @@ nav {
align-items: center;
gap: 0.35rem;
padding: 0.3rem;
border: 1px solid rgb(35 30 30 / 10%);
border-radius: 999px;
background: rgb(255 255 255 / 42%);
backdrop-filter: blur(12px);
border: 3px solid #1c1917;
border-radius: 0;
background: #ffffff;
}

.nav-link {
Expand Down Expand Up @@ -732,3 +734,20 @@ dd {
margin-top: 0.9rem;
}
}

.lang-switch {
display: inline-flex;
gap: 0.25rem;
margin-left: 0.5rem;
}
.lang-switch button {
padding: 0.45rem 0.75rem;
background: #ffffff;
color: #1c1917;
border: 3px solid #1c1917;
border-radius: 0;
font-weight: 700;
}
.lang-switch button.on {
background: #fbbf24;
}
5 changes: 5 additions & 0 deletions desktop/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
}
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700;800&display=swap"
rel="stylesheet"
/>
</head>

<body>
Expand Down
27 changes: 22 additions & 5 deletions desktop/src/app/AppTopChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PanelLeftOpen,
} from "lucide-react";

import { LanguageToggle, useI18n } from "@/shared/i18n";
import { isMacPlatform } from "@/shared/lib/platform";
import { useIsFullscreen } from "@/shared/lib/useIsFullscreen";
import { Button } from "@/shared/ui/button";
Expand Down Expand Up @@ -36,10 +37,11 @@ function preventTopChromeWheel(event: WheelEvent) {

function TopChromeSidebarTrigger() {
const sidebar = useOptionalSidebar();
const { t } = useI18n();

return (
<Button
aria-label="Toggle Sidebar"
aria-label={t("chrome.toggleSidebar")}
className={TOP_CHROME_ICON_BUTTON_CLASS}
data-sidebar="trigger"
disabled={!sidebar}
Expand All @@ -51,7 +53,7 @@ function TopChromeSidebarTrigger() {
variant="ghost"
>
{sidebar?.open ? <PanelLeftClose /> : <PanelLeftOpen />}
<span className="sr-only">Toggle Sidebar</span>
<span className="sr-only">{t("chrome.toggleSidebar")}</span>
</Button>
);
}
Expand All @@ -63,6 +65,7 @@ export function AppTopChrome({
onGoForward,
hasCommunityRail = false,
}: AppTopChromeProps) {
const { t } = useI18n();
const topChromeRef = React.useRef<HTMLDivElement>(null);
const isFullscreen = useIsFullscreen();
// On macOS the traffic-light buttons overlay the chrome (see
Expand Down Expand Up @@ -106,10 +109,15 @@ export function AppTopChrome({
data-tauri-drag-region
data-testid="app-top-chrome"
>
<div className={cn("flex items-center gap-0.5", navRowAlignmentClass)}>
<div
className={cn(
"flex min-w-0 flex-1 items-center gap-0.5",
navRowAlignmentClass,
)}
>
<TopChromeSidebarTrigger />
<Button
aria-label="Go back"
aria-label={t("chrome.back")}
className={HISTORY_ICON_BUTTON_CLASS}
data-testid="global-back"
disabled={!canGoBack}
Expand All @@ -120,7 +128,7 @@ export function AppTopChrome({
<ChevronLeft />
</Button>
<Button
aria-label="Go forward"
aria-label={t("chrome.forward")}
className={HISTORY_ICON_BUTTON_CLASS}
data-testid="global-forward"
disabled={!canGoForward}
Expand All @@ -131,6 +139,15 @@ export function AppTopChrome({
<ChevronRight />
</Button>
</div>
<div
className={cn(
"ml-auto flex shrink-0 items-center",
navRowAlignmentClass,
)}
data-tauri-drag-region="false"
>
<LanguageToggle />
</div>
</div>
);
}
Loading