feat(search): ephemeral results UX, count selector, channel blocklist (frontend)
- Live-search view: a results-count selector (20/40/60/100) replaces manual load-more (the free scrape source pages until that many are gathered); an 'these results are temporary' banner with a 'Clear now' button that discards them 'as if never added' (api.clearSearch) and returns to the feed. - Channel blocklist: a Block/Unblock toggle + 'Blocked' badge on the channel page (blocked channels don't auto-explore and their videos are hidden), and a 'Blocked channels' section in the Channel manager with one-click unblock. ChannelDetail.blocked from the backend. - Admin: a 'Purge discovery' button on the Scheduler page (immediate un-kept search/explore cleanup). EN/HU/DE throughout.
This commit is contained in:
parent
f27f31b6db
commit
0c18845c34
21 changed files with 263 additions and 65 deletions
|
|
@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Ban,
|
||||
Check,
|
||||
Eye,
|
||||
EyeOff,
|
||||
|
|
@ -87,7 +88,15 @@ export default function Channels({
|
|||
const channelsQuery = useQuery({ queryKey: ["channels"], queryFn: api.channels });
|
||||
const tagsQuery = useQuery({ queryKey: ["tags"], queryFn: api.tags });
|
||||
const statusQuery = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus });
|
||||
const blockedQuery = useQuery({ queryKey: ["blocked-channels"], queryFn: api.blockedChannels });
|
||||
const [tagManagerOpen, setTagManagerOpen] = useState(false);
|
||||
const unblock = useMutation({
|
||||
mutationFn: (id: string) => api.unblockChannel(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["blocked-channels"] });
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
},
|
||||
});
|
||||
|
||||
const invalidate = () => {
|
||||
qc.invalidateQueries({ queryKey: ["channels"] });
|
||||
|
|
@ -567,6 +576,37 @@ export default function Channels({
|
|||
emptyText={t("channels.empty")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Blocked channels — videos from these are hidden everywhere and dropped from live search. */}
|
||||
{(blockedQuery.data?.length ?? 0) > 0 && (
|
||||
<div className="mt-6 border-t border-border pt-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Ban className="w-4 h-4 text-muted" />
|
||||
<span className="text-xs uppercase tracking-wide text-muted">
|
||||
{t("channels.blocked.title")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{blockedQuery.data!.map((c) => (
|
||||
<span
|
||||
key={c.id}
|
||||
className="inline-flex items-center gap-1.5 text-xs pl-2.5 pr-1 py-1 rounded-full bg-card border border-border"
|
||||
>
|
||||
{c.title ?? c.id}
|
||||
<button
|
||||
onClick={() => unblock.mutate(c.id)}
|
||||
title={t("channel.unblock")}
|
||||
aria-label={t("channel.unblock")}
|
||||
className="rounded-full p-0.5 text-muted hover:text-danger transition"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[11px] text-muted mt-2">{t("channels.blocked.hint")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue