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:
parent
e07a37622d
commit
ecbecbb9f4
6 changed files with 115 additions and 30 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue