feat: feedback round 3 — content-type toggles, channel filter, card polish

- Content type is now three independent toggles (Normal / Shorts / Live·Upcoming);
  the feed is the union of enabled types (backend show_normal + include_shorts +
  include_live)
- Per-channel filter: a button on each card scopes the feed to that channel, shown
  as a removable chip in the sidebar
- Hide now refetches the feed once the change is persisted, so a hidden video shows
  up in the Hidden view immediately (no refresh needed); optimistic updates respect
  the current view
- Grid cards are now distinct panels (card background, border, shadow) that lift on
  hover; clickable channel name in list view too
This commit is contained in:
npeter83 2026-06-11 03:47:51 +02:00
parent e07a37622d
commit ecbecbb9f4
6 changed files with 115 additions and 30 deletions

View file

@ -1,4 +1,4 @@
import { Bookmark, Check, EyeOff } from "lucide-react";
import { Bookmark, Check, EyeOff, ListFilter } from "lucide-react";
import clsx from "clsx";
import type { Video } from "../lib/api";
import { formatDuration, formatViews, relativeTime } from "../lib/format";
@ -6,9 +6,11 @@ import { formatDuration, formatViews, relativeTime } from "../lib/format";
function Actions({
video,
onState,
onChannelFilter,
}: {
video: Video;
onState: (id: string, status: string) => void;
onChannelFilter?: (channelId: string, channelName: string) => void;
}) {
const act = (status: string) => (e: React.MouseEvent) => {
e.preventDefault();
@ -21,7 +23,7 @@ function Actions({
onClick={act("watched")}
title="Mark watched"
className={clsx(
"p-1.5 rounded-md hover:bg-card text-muted hover:text-fg",
"p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg",
video.status === "watched" && "text-accent"
)}
>
@ -31,7 +33,7 @@ function Actions({
onClick={act("saved")}
title="Save for later"
className={clsx(
"p-1.5 rounded-md hover:bg-card text-muted hover:text-fg",
"p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg",
video.status === "saved" && "text-accent"
)}
>
@ -40,10 +42,23 @@ function Actions({
<button
onClick={act("hidden")}
title="Hide"
className="p-1.5 rounded-md hover:bg-card text-muted hover:text-fg"
className="p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg"
>
<EyeOff className="w-4 h-4" />
</button>
{onChannelFilter && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onChannelFilter(video.channel_id, video.channel_title ?? "This channel");
}}
title="Only this channel"
className="p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg"
>
<ListFilter className="w-4 h-4" />
</button>
)}
</div>
);
}
@ -93,10 +108,12 @@ export default function VideoCard({
video,
view,
onState,
onChannelFilter,
}: {
video: Video;
view: "grid" | "list";
onState: (id: string, status: string) => void;
onChannelFilter?: (channelId: string, channelName: string) => void;
}) {
const watched = video.status === "watched";
const meta = (
@ -138,7 +155,7 @@ export default function VideoCard({
</a>
<div className="text-xs text-muted mt-0.5">{meta}</div>
</div>
<Actions video={video} onState={onState} />
<Actions video={video} onState={onState} onChannelFilter={onChannelFilter} />
</div>
);
}
@ -146,12 +163,12 @@ export default function VideoCard({
return (
<div
className={clsx(
"group transition-transform duration-150 hover:-translate-y-1",
"group rounded-2xl border border-border bg-card/50 p-2.5 shadow-sm transition-all duration-150 hover:-translate-y-1 hover:shadow-2xl hover:bg-card hover:border-accent/40",
watched && "opacity-55"
)}
>
<Thumb video={video} className="aspect-video" />
<div className="flex gap-3 mt-2">
<div className="flex gap-3 mt-2.5 px-0.5 pb-0.5">
{video.channel_thumbnail && (
<img
src={video.channel_thumbnail}
@ -172,7 +189,7 @@ export default function VideoCard({
{video.channel_title}
</a>
<div className="text-xs text-muted mt-0.5">{meta}</div>
<Actions video={video} onState={onState} />
<Actions video={video} onState={onState} onChannelFilter={onChannelFilter} />
</div>
</div>
</div>