Login screen (with a language picker), header, account menu, sync status, About and Release Notes dialogs, and the version banner are now fully translated in Hungarian, English and German.
191 lines
6.3 KiB
TypeScript
191 lines
6.3 KiB
TypeScript
import { useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { BarChart3, Home, Info, LogOut, Search, Settings, Shield, Tv } from "lucide-react";
|
|
import type { FeedFilters, Me } from "../lib/api";
|
|
import type { Page } from "../lib/urlState";
|
|
import { type LangCode } from "../i18n";
|
|
import SyncStatus from "./SyncStatus";
|
|
import NotificationCenter from "./NotificationCenter";
|
|
import LanguageSwitcher from "./LanguageSwitcher";
|
|
import AvatarImg from "./Avatar";
|
|
|
|
export default function Header({
|
|
me,
|
|
filters,
|
|
setFilters,
|
|
page,
|
|
setPage,
|
|
onOpenSettings,
|
|
onOpenAbout,
|
|
onChangeLanguage,
|
|
onGoToFullHistory,
|
|
}: {
|
|
me: Me;
|
|
filters: FeedFilters;
|
|
setFilters: (f: FeedFilters) => void;
|
|
page: Page;
|
|
setPage: (p: Page) => void;
|
|
onOpenSettings: () => void;
|
|
onOpenAbout: () => void;
|
|
onChangeLanguage: (code: LangCode) => void;
|
|
onGoToFullHistory: () => void;
|
|
}) {
|
|
const { t, i18n } = useTranslation();
|
|
|
|
async function logout() {
|
|
await fetch("/auth/logout", { method: "POST", credentials: "include" });
|
|
location.reload();
|
|
}
|
|
|
|
return (
|
|
<header className="h-14 shrink-0 border-b border-border bg-surface/95 flex items-center gap-3 px-4 z-20">
|
|
<button
|
|
onClick={() => setPage("feed")}
|
|
className="text-xl font-bold tracking-tight select-none"
|
|
title={t("header.feed")}
|
|
>
|
|
Sift<span className="text-accent">lode</span>
|
|
</button>
|
|
|
|
<SyncStatus isAdmin={me.role === "admin"} onGoToFullHistory={onGoToFullHistory} />
|
|
|
|
{page === "feed" ? (
|
|
<div className="flex-1 max-w-xl mx-auto relative">
|
|
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted" />
|
|
<input
|
|
value={filters.q}
|
|
onChange={(e) => setFilters({ ...filters, q: e.target.value })}
|
|
placeholder={t("header.searchPlaceholder")}
|
|
className="w-full bg-card border border-border rounded-full pl-9 pr-4 py-2 text-sm outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 text-center text-sm font-semibold">
|
|
{page === "stats" ? t("header.usageStats") : t("header.channelManager")}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-1">
|
|
<LanguageSwitcher value={i18n.language as LangCode} onChange={onChangeLanguage} />
|
|
<NotificationCenter filters={filters} setFilters={setFilters} />
|
|
<div className="pl-1">
|
|
<AccountMenu
|
|
me={me}
|
|
logout={logout}
|
|
page={page}
|
|
setPage={setPage}
|
|
onOpenSettings={onOpenSettings}
|
|
onOpenAbout={onOpenAbout}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
function Avatar({ me, className = "" }: { me: Me; className?: string }) {
|
|
return (
|
|
<AvatarImg
|
|
src={me.avatar_url}
|
|
fallback={me.display_name ?? me.email}
|
|
className={`rounded-full ${className}`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function AccountMenu({
|
|
me,
|
|
logout,
|
|
page,
|
|
setPage,
|
|
onOpenSettings,
|
|
onOpenAbout,
|
|
}: {
|
|
me: Me;
|
|
logout: () => void;
|
|
page: Page;
|
|
setPage: (p: Page) => void;
|
|
onOpenSettings: () => void;
|
|
onOpenAbout: () => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [open, setOpen] = useState(false);
|
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
function openNow() {
|
|
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
setOpen(true);
|
|
}
|
|
function closeSoon() {
|
|
closeTimer.current = setTimeout(() => setOpen(false), 220);
|
|
}
|
|
|
|
const itemClass =
|
|
"w-full flex items-center gap-2 text-sm px-2 py-2 rounded-lg text-muted hover:text-fg hover:bg-card transition";
|
|
|
|
return (
|
|
<div className="relative" onMouseEnter={openNow} onMouseLeave={closeSoon}>
|
|
<button
|
|
onClick={() => setOpen((o) => !o)}
|
|
title={me.email}
|
|
className="block rounded-full ring-2 ring-transparent hover:ring-border focus:outline-none focus:ring-accent transition"
|
|
>
|
|
<Avatar me={me} className="w-8 h-8 text-xs" />
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="glass absolute right-0 mt-2 w-64 rounded-xl p-3 z-30 animate-[popIn_0.16s_ease]">
|
|
<div className="flex items-center gap-3 pb-3 border-b border-border">
|
|
<Avatar me={me} className="w-10 h-10 text-sm shrink-0" />
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-semibold truncate">
|
|
{me.display_name ?? me.email.split("@")[0]}
|
|
</div>
|
|
<div className="text-xs text-muted truncate">{me.email}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{me.role === "admin" && (
|
|
<div className="flex items-center gap-1.5 mt-2 text-[11px] font-medium text-accent">
|
|
<Shield className="w-3.5 h-3.5" />
|
|
{t("header.account.admin")}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-2 flex flex-col">
|
|
{page !== "feed" && (
|
|
<button onClick={() => { setPage("feed"); setOpen(false); }} className={itemClass}>
|
|
<Home className="w-4 h-4" />
|
|
{t("header.account.feed")}
|
|
</button>
|
|
)}
|
|
{page !== "channels" && (
|
|
<button onClick={() => { setPage("channels"); setOpen(false); }} className={itemClass}>
|
|
<Tv className="w-4 h-4" />
|
|
{t("header.account.channels")}
|
|
</button>
|
|
)}
|
|
{me.role === "admin" && page !== "stats" && (
|
|
<button onClick={() => { setPage("stats"); setOpen(false); }} className={itemClass}>
|
|
<BarChart3 className="w-4 h-4" />
|
|
{t("header.account.stats")}
|
|
</button>
|
|
)}
|
|
<button onClick={() => { onOpenSettings(); setOpen(false); }} className={itemClass}>
|
|
<Settings className="w-4 h-4" />
|
|
{t("header.account.settings")}
|
|
</button>
|
|
<button onClick={() => { onOpenAbout(); setOpen(false); }} className={itemClass}>
|
|
<Info className="w-4 h-4" />
|
|
{t("header.account.about")}
|
|
</button>
|
|
<button onClick={logout} className={itemClass}>
|
|
<LogOut className="w-4 h-4" />
|
|
{t("header.account.signOut")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|