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:
npeter83 2026-06-30 03:08:52 +02:00
parent bc4c362423
commit cc1e670202
18 changed files with 744 additions and 120 deletions

View file

@ -77,6 +77,7 @@ export default function Feed({
ytSearch,
onYtSearch,
onExitYtSearch,
onOpenChannel,
}: {
filters: FeedFilters;
setFilters: (f: FeedFilters) => void;
@ -91,6 +92,9 @@ export default function Feed({
ytSearch: string | null;
onYtSearch: (q: string) => void;
onExitYtSearch: () => void;
// Open a channel's dedicated page (from a card's channel name / avatar). Threaded down to
// the cards via VirtualFeed.
onOpenChannel?: (channelId: string, channelName?: string) => void;
}) {
const { t } = useTranslation();
const [overrides, setOverrides] = useState<Record<string, string>>({});
@ -257,11 +261,11 @@ export default function Feed({
[qc]
);
const onChannelFilter = useCallback(
const handleOpenChannel = useCallback(
(channelId: string, channelName: string) => {
setFilters({ ...filters, channelId, channelName });
onOpenChannel?.(channelId, channelName);
},
[filters, setFilters]
[onOpenChannel]
);
const onToggleSave = useCallback(
@ -347,7 +351,7 @@ export default function Feed({
view={view}
onState={onState}
onToggleSave={onToggleSave}
onChannelFilter={onChannelFilter}
onOpenChannel={handleOpenChannel}
onResetState={onResetState}
onOpen={openVideo}
hasNextPage={false}
@ -568,7 +572,7 @@ export default function Feed({
view={view}
onState={onState}
onToggleSave={onToggleSave}
onChannelFilter={onChannelFilter}
onOpenChannel={handleOpenChannel}
onResetState={onResetState}
onOpen={openVideo}
hasNextPage={!!hasNextPage}