feat(stats): add library entry merge and episode move

- Add multi-select "Merge Selected" flow to fold duplicate library cards into one, preserving sessions, mined cards, and watch time
- Add per-episode "move to another entry" action for reassigning stray episodes, pruning the source entry when emptied
- Auto-merge library entries that resolve to the same AniList id when their parsed seasons are compatible
- Add mergeAnime/moveVideoToAnime service methods, stats-server routes, and HTTP contract types
This commit is contained in:
2026-08-10 22:52:34 -07:00
parent 2fefc83e3f
commit 5938095d92
19 changed files with 1629 additions and 9 deletions
+26 -3
View File
@@ -5,22 +5,45 @@ import type { AnimeLibraryItem } from '../../types/stats';
interface AnimeCardProps {
anime: AnimeLibraryItem;
onClick: () => void;
/** While selecting, clicking the card toggles it instead of opening it. */
selectable?: boolean;
selected?: boolean;
}
export function AnimeCard({ anime, onClick }: AnimeCardProps) {
export function AnimeCard({
anime,
onClick,
selectable = false,
selected = false,
}: AnimeCardProps) {
return (
<button
type="button"
onClick={onClick}
className="group bg-ctp-surface0 border border-ctp-surface1 rounded-lg overflow-hidden hover:border-ctp-blue/50 hover:shadow-lg hover:shadow-ctp-blue/10 transition-all duration-200 hover:-translate-y-1 text-left w-full"
aria-pressed={selectable ? selected : undefined}
className={`group bg-ctp-surface0 border rounded-lg overflow-hidden hover:shadow-lg hover:shadow-ctp-blue/10 transition-all duration-200 hover:-translate-y-1 text-left w-full ${
selected ? 'border-ctp-blue' : 'border-ctp-surface1 hover:border-ctp-blue/50'
}`}
>
<div className="overflow-hidden">
<div className="overflow-hidden relative">
<AnimeCoverImage
animeId={anime.animeId}
title={anime.canonicalTitle}
coverRetryToken={anime.anilistId ?? 0}
className="w-full aspect-[3/4] rounded-t-lg transition-transform duration-200 group-hover:scale-105"
/>
{selectable && (
<span
aria-hidden="true"
className={`absolute top-2 left-2 w-5 h-5 rounded border flex items-center justify-center text-xs ${
selected
? 'bg-ctp-blue border-ctp-blue text-ctp-base'
: 'bg-ctp-crust/70 border-ctp-surface2 text-transparent'
}`}
>
{'✓'}
</span>
)}
</div>
<div className="p-3">
<div className="text-sm font-medium text-ctp-text truncate">{anime.canonicalTitle}</div>