feat(channels): dedicated channel page + ephemeral explore UI
Frontend for the channel-explore feature: - ChannelPage: banner/avatar/stats header, Subscribe/unsubscribe, an "exploring" badge while browsing an un-subscribed channel, Videos/About tabs. Reuses Feed scoped to the channel (scope=all + source=all so the per-user ephemeral videos show). Auto-ingests recent uploads on first visit (background, with a loading note) + "Load more from YouTube" to page deeper; skipped for demo / already-subscribed channels. - App: openChannel/closeChannel as a Back-aware sub-view (history.state._chan, mirrors the YT-search _yt pattern); ChannelPage takes over the content column, nav rail stays. - ChannelLink/cards/player: the channel name now opens our channel page (onChannelFilter → onOpenChannel); the in-card "only this channel" filter button is dropped (the page subsumes it). PlayerModal channel-name wiring follows in the next commit. - api: channelDetail + exploreChannel; ChannelDetail/ExploreResult types. - i18n EN/HU/DE: channel namespace, explore_cleanup scheduler labels, explore config group, channels_explore quota label.
This commit is contained in:
parent
bc4c362423
commit
cc1e670202
18 changed files with 744 additions and 120 deletions
|
|
@ -6,7 +6,6 @@ import {
|
|||
CheckCheck,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ListFilter,
|
||||
Play,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
|
|
@ -21,13 +20,11 @@ function Actions({
|
|||
onState,
|
||||
onResetState,
|
||||
onToggleSave,
|
||||
onChannelFilter,
|
||||
}: {
|
||||
video: Video;
|
||||
onState: (id: string, status: string) => void;
|
||||
onResetState?: (id: string) => void;
|
||||
onToggleSave: (id: string, saved: boolean) => void;
|
||||
onChannelFilter?: (channelId: string, channelName: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const act = (status: string) => (e: React.MouseEvent) => {
|
||||
|
|
@ -97,19 +94,6 @@ function Actions({
|
|||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{onChannelFilter && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onChannelFilter(video.channel_id, video.channel_title ?? t("card.thisChannel"));
|
||||
}}
|
||||
title={t("card.onlyThisChannel")}
|
||||
className="p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg"
|
||||
>
|
||||
<ListFilter className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -244,7 +228,7 @@ function VideoCard({
|
|||
onState,
|
||||
onResetState,
|
||||
onToggleSave,
|
||||
onChannelFilter,
|
||||
onOpenChannel,
|
||||
onOpen,
|
||||
}: {
|
||||
video: Video;
|
||||
|
|
@ -252,7 +236,7 @@ function VideoCard({
|
|||
onState: (id: string, status: string) => void;
|
||||
onResetState?: (id: string) => void;
|
||||
onToggleSave: (id: string, saved: boolean) => void;
|
||||
onChannelFilter?: (channelId: string, channelName: string) => void;
|
||||
onOpenChannel?: (channelId: string, channelName: string) => void;
|
||||
onOpen?: (v: Video, startAt?: number | null) => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
|
@ -303,18 +287,18 @@ function VideoCard({
|
|||
<Thumb video={video} className="w-44 aspect-video shrink-0" onOpen={onOpen} />
|
||||
<div className="min-w-0 flex-1">
|
||||
{title}
|
||||
<a
|
||||
href={video.channel_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-sm text-muted truncate mt-0.5 block w-fit max-w-full hover:text-fg"
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenChannel?.(video.channel_id, video.channel_title ?? t("card.thisChannel"));
|
||||
}}
|
||||
className="text-sm text-muted truncate mt-0.5 block w-fit max-w-full text-left hover:text-fg"
|
||||
>
|
||||
{video.channel_title}
|
||||
</a>
|
||||
</button>
|
||||
<div className="text-xs text-muted mt-0.5">{meta}</div>
|
||||
</div>
|
||||
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} onChannelFilter={onChannelFilter} />
|
||||
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -335,17 +319,17 @@ function VideoCard({
|
|||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
{title}
|
||||
<a
|
||||
href={video.channel_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-sm text-muted truncate mt-0.5 block w-fit max-w-full hover:text-fg"
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenChannel?.(video.channel_id, video.channel_title ?? t("card.thisChannel"));
|
||||
}}
|
||||
className="text-sm text-muted truncate mt-0.5 block w-fit max-w-full text-left hover:text-fg"
|
||||
>
|
||||
{video.channel_title}
|
||||
</a>
|
||||
</button>
|
||||
<div className="text-xs text-muted mt-0.5">{meta}</div>
|
||||
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} onChannelFilter={onChannelFilter} />
|
||||
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue