mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 03:55:38 -04:00
Status (watchlist/watching/completed) for TV shows is now automatically determined by episode progress rather than requiring manual selection from a dropdown. The only manual action is adding/removing from watchlist — watching and completed states flow from episode data. - Decouple markAllEpisodesWatched from setTitleStatus and export it - Replace status dropdown with toggle button + read-only status badge - Add "Mark All Watched" button with confirmation dialog for TV shows - Fix optimistic transitions (watchlist → watching on first episode) - Simplify W keyboard shortcut to toggle watchlist on/off - Add optimistic completion check when marking entire seasons Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
IconBookmarkFilled,
|
|
IconCheck,
|
|
IconPlayerPlayFilled,
|
|
IconPlus,
|
|
IconX,
|
|
} from "@tabler/icons-react";
|
|
import { AnimatePresence, motion } from "motion/react";
|
|
|
|
const statusConfig = {
|
|
watchlist: {
|
|
label: "Watchlist",
|
|
icon: IconBookmarkFilled,
|
|
class: "text-status-watchlist",
|
|
bgClass: "bg-status-watchlist/10 hover:bg-status-watchlist/15",
|
|
borderClass: "ring-status-watchlist/20",
|
|
},
|
|
in_progress: {
|
|
label: "Watching",
|
|
icon: IconPlayerPlayFilled,
|
|
class: "text-status-watching",
|
|
bgClass: "bg-status-watching/10 hover:bg-status-watching/15",
|
|
borderClass: "ring-status-watching/20",
|
|
},
|
|
completed: {
|
|
label: "Completed",
|
|
icon: IconCheck,
|
|
class: "text-status-completed",
|
|
bgClass: "bg-status-completed/10 hover:bg-status-completed/15",
|
|
borderClass: "ring-status-completed/20",
|
|
},
|
|
} as const;
|
|
|
|
interface StatusButtonProps {
|
|
currentStatus: string | null;
|
|
onChange: (status: string | null) => void;
|
|
}
|
|
|
|
export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
|
|
const config =
|
|
statusConfig[currentStatus as keyof typeof statusConfig] ?? null;
|
|
|
|
return (
|
|
<AnimatePresence mode="wait" initial={false}>
|
|
{!config ? (
|
|
<motion.button
|
|
key="add"
|
|
type="button"
|
|
onClick={() => onChange("watchlist")}
|
|
initial={{ opacity: 0, y: 4 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -4 }}
|
|
transition={{ duration: 0.15 }}
|
|
className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary/10 px-4 text-sm font-medium text-primary ring-1 ring-primary/20 transition-all hover:bg-primary/15 hover:ring-primary/30 active:scale-[0.97]"
|
|
>
|
|
<IconPlus size={14} strokeWidth={2.5} />
|
|
Watchlist
|
|
</motion.button>
|
|
) : (
|
|
<motion.button
|
|
key="status"
|
|
type="button"
|
|
onClick={() => onChange(null)}
|
|
initial={{ opacity: 0, y: 4 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -4 }}
|
|
transition={{ duration: 0.15 }}
|
|
title="Remove from library"
|
|
className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:ring-destructive/30 hover:bg-destructive/10 hover:text-destructive`}
|
|
>
|
|
<span className="grid [&>svg]:col-start-1 [&>svg]:row-start-1">
|
|
<config.icon
|
|
size={14}
|
|
className="transition-opacity group-hover:opacity-0"
|
|
/>
|
|
<IconX
|
|
size={14}
|
|
className="opacity-0 text-destructive transition-opacity group-hover:opacity-100"
|
|
/>
|
|
</span>
|
|
<span className="grid [&>span]:col-start-1 [&>span]:row-start-1">
|
|
<span className="transition-opacity group-hover:opacity-0">
|
|
{config.label}
|
|
</span>
|
|
<span className="opacity-0 transition-opacity group-hover:opacity-100">
|
|
Remove
|
|
</span>
|
|
</span>
|
|
</motion.button>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|