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
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowLeft, Check, ExternalLink, Loader2, Plus, RefreshCw } from "lucide-react";
|
||||
import { ArrowLeft, Ban, Check, ExternalLink, Loader2, Plus, RefreshCw } from "lucide-react";
|
||||
import Avatar from "./Avatar";
|
||||
import Feed from "./Feed";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
|
|
@ -68,12 +68,21 @@ export default function ChannelPage({
|
|||
useEffect(() => {
|
||||
if (!ch || me.is_demo) return;
|
||||
if (autoTried.current === channelId) return;
|
||||
if (ch.subscribed) return;
|
||||
if (ch.subscribed || ch.blocked) return;
|
||||
if (ch.explored && ch.stored_videos > 0) return;
|
||||
autoTried.current = channelId;
|
||||
runExplore().catch(() => {});
|
||||
}, [ch, channelId, me.is_demo, runExplore]);
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: () => (ch?.blocked ? api.unblockChannel(channelId) : api.blockChannel(channelId)),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["channel", channelId] });
|
||||
qc.invalidateQueries({ queryKey: ["feed"] });
|
||||
qc.invalidateQueries({ queryKey: ["blocked-channels"] });
|
||||
},
|
||||
});
|
||||
|
||||
const subscribe = useMutation({
|
||||
mutationFn: () => api.subscribeChannel(channelId),
|
||||
onSuccess: () => {
|
||||
|
|
@ -178,10 +187,17 @@ export default function ChannelPage({
|
|||
<div className="flex-1 min-w-0 pb-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h1 className="text-xl font-semibold truncate">{name}</h1>
|
||||
{ch?.explored && !ch?.subscribed && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-amber-500/15 text-amber-600 dark:text-amber-400">
|
||||
{t("channel.exploringBadge")}
|
||||
{ch?.blocked ? (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-danger/15 text-danger">
|
||||
{t("channel.blockedBadge")}
|
||||
</span>
|
||||
) : (
|
||||
ch?.explored &&
|
||||
!ch?.subscribed && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-amber-500/15 text-amber-600 dark:text-amber-400">
|
||||
{t("channel.exploringBadge")}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted mt-0.5 flex flex-wrap gap-x-1.5 gap-y-0.5">
|
||||
|
|
@ -204,7 +220,23 @@ export default function ChannelPage({
|
|||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
{!me.is_demo && (
|
||||
<button
|
||||
onClick={() => block.mutate()}
|
||||
disabled={block.isPending}
|
||||
title={ch?.blocked ? t("channel.unblock") : t("channel.block")}
|
||||
aria-label={ch?.blocked ? t("channel.unblock") : t("channel.block")}
|
||||
className={`inline-flex items-center justify-center w-9 h-9 rounded-lg border disabled:opacity-50 transition ${
|
||||
ch?.blocked
|
||||
? "border-danger text-danger"
|
||||
: "border-border text-muted hover:text-danger hover:border-danger"
|
||||
}`}
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{!me.is_demo &&
|
||||
!ch?.blocked &&
|
||||
(ch?.subscribed ? (
|
||||
<button
|
||||
onClick={onUnsubscribe}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue