siftlode/frontend/src/components/Header.tsx
npeter83 73b06b1fb6 feat(scheduler): live admin Scheduler dashboard + reusable poll hook (HU/EN/DE)
New admin Scheduler page (left-nav entry) with a live, self-refreshing view of
job activity, queued work and quota. Polling is factored into a reusable
useLiveQuery hook (pauses when the tab is unfocused) that the notification bell
and future yt-dlp job queue will reuse instead of re-implementing.
2026-06-16 14:38:51 +02:00

93 lines
3.4 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Library, Search, User } 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";
// Contextual top bar. Primary navigation + account now live in the left NavSidebar; the
// header carries the global sync status, the feed's scope toggle + search (or the current
// page title), the language switcher and the notification bell.
export default function Header({
me,
filters,
setFilters,
page,
onChangeLanguage,
onGoToFullHistory,
}: {
me: Me;
filters: FeedFilters;
setFilters: (f: FeedFilters) => void;
page: Page;
onChangeLanguage: (code: LangCode) => void;
onGoToFullHistory: () => void;
}) {
const { t, i18n } = useTranslation();
return (
<header className="glass h-14 shrink-0 border-b border-border flex items-center gap-3 px-4 z-20">
<SyncStatus isAdmin={me.role === "admin"} onGoToFullHistory={onGoToFullHistory} />
{page === "feed" && (
<div
className="flex items-center rounded-full border border-border bg-card p-0.5 text-xs shrink-0"
role="group"
aria-label={t("header.scope.label")}
>
{(["my", "all"] as const).map((s) => (
<button
key={s}
onClick={() => setFilters({ ...filters, scope: s })}
title={t("header.scope." + s + "Tip")}
aria-pressed={filters.scope === s}
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full transition ${
filters.scope === s
? "bg-accent text-accent-fg"
: "text-muted hover:text-fg"
}`}
>
{s === "my" ? (
<User className="w-3.5 h-3.5" />
) : (
<Library className="w-3.5 h-3.5" />
)}
<span className="hidden sm:inline">{t("header.scope." + s)}</span>
</button>
))}
</div>
)}
{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")
: page === "playlists"
? t("header.account.playlists")
: page === "settings"
? t("settings.title")
: page === "scheduler"
? t("header.scheduler")
: 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>
</header>
);
}