feat(tags): tag UX overhaul + per-card video reset

- Channel-row tags show only what's attached; a '+' opens a per-channel picker. Clicking a tag
  filters the feed by it; a 'Your tags' sidebar widget makes that filter visible/clearable.
- Tag manager dialog (add/rename/delete; delete only confirms when the tag is in use), reachable
  from the Channel manager and the feed sidebar; hovering a tag's count lists its channels, each a
  link that focuses it in the Channel manager (DataTable gains an external filter input).
- Video cards get a reset action that clears all watch state (incl. an in-progress position).
- api.req() now raises the error dialog on 5xx and 400/409/422 with the server's reason.
This commit is contained in:
npeter83 2026-06-18 01:17:31 +02:00
parent 14b8eb6084
commit d33a109ea2
22 changed files with 445 additions and 49 deletions

View file

@ -32,6 +32,7 @@ import SettingsPanel from "./components/SettingsPanel";
import OnboardingWizard from "./components/OnboardingWizard";
import { shouldAutoOpenOnboarding } from "./lib/onboarding";
import Toaster from "./components/Toaster";
import ErrorDialog from "./components/ErrorDialog";
import About from "./components/About";
import ReleaseNotes from "./components/ReleaseNotes";
import VersionBanner from "./components/VersionBanner";
@ -108,6 +109,16 @@ export default function App() {
const [aboutOpen, setAboutOpen] = useState(false);
const [notesOpen, setNotesOpen] = useState(false);
const [notesHighlight, setNotesHighlight] = useState<string | undefined>(undefined);
// "Focus this channel in the manager": jump to the Channels page and seed its name filter
// so the channel is isolated. Cleared when leaving the page so it doesn't re-apply later.
const [focusChannelName, setFocusChannelName] = useState<string | null>(null);
const focusChannel = (name: string) => {
setFocusChannelName(name);
setPage("channels");
};
useEffect(() => {
if (page !== "channels") setFocusChannelName(null);
}, [page]);
function openReleaseNotes(highlight?: string) {
setNotesHighlight(highlight);
@ -271,6 +282,7 @@ export default function App() {
setFilters={setFilters}
layout={sidebarLayout}
setLayout={setSidebarLayout}
onFocusChannel={focusChannel}
/>
)}
<main className="flex-1 min-w-0 overflow-y-auto">
@ -278,6 +290,8 @@ export default function App() {
<Channels
canWrite={meQuery.data!.can_write}
isAdmin={meQuery.data!.role === "admin"}
focusChannelName={focusChannelName}
onFocusChannel={focusChannel}
statusFilter={channelFilter}
setStatusFilter={setChannelFilter}
onOpenWizard={() => setWizardOpen(true)}
@ -285,6 +299,11 @@ export default function App() {
setFilters({ ...filters, channelId: id, channelName: name, show: "all" });
setPage("feed");
}}
onFilterByTag={(tagId, name) => {
setFilters({ ...filters, tags: [tagId], channelId: undefined, channelName: undefined });
setPage("feed");
notify({ level: "info", message: t("channels.notify.filteredByTag", { name }) });
}}
/>
) : page === "stats" && meQuery.data!.role === "admin" ? (
<Stats />
@ -333,6 +352,7 @@ export default function App() {
{notesOpen && (
<ReleaseNotes onClose={() => setNotesOpen(false)} highlight={notesHighlight} />
)}
<ErrorDialog />
</div>
);
}