Files
stripstream-librarian/apps/backoffice/app/components/MarkSeriesReadButton.tsx
Froidefond Julien e94a4a0b13 feat: AniList reading status integration
- Add full AniList integration: OAuth connect, series linking, push/pull sync
- Push: PLANNING/CURRENT/COMPLETED based on books read vs total_volumes (never auto-complete from owned books alone)
- Pull: update local reading progress from AniList list (per-user)
- Detailed sync/pull reports with per-series status and progress
- Local user selector in settings to scope sync to a specific user
- Rename "AniList" tab/buttons to generic "État de lecture" / "Reading status"
- Make Bédéthèque and AniList badges clickable links on series detail page
- Fix ON CONFLICT error on series link (provider column in PK)
- Migration 0054: fix series_metadata missing columns (authors, publishers, locked_fields, total_volumes, status)
- Align button heights on series detail page; move MarkSeriesReadButton to action row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 17:08:11 +01:00

77 lines
2.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "../../lib/i18n/context";
interface MarkSeriesReadButtonProps {
seriesName: string;
bookCount: number;
booksReadCount: number;
}
export function MarkSeriesReadButton({ seriesName, bookCount, booksReadCount }: MarkSeriesReadButtonProps) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const router = useRouter();
const allRead = booksReadCount >= bookCount;
const targetStatus = allRead ? "unread" : "read";
const label = allRead ? t("markRead.markUnread") : t("markRead.markAllRead");
const handleClick = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setLoading(true);
try {
const res = await fetch("/api/series/mark-read", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ series: seriesName, status: targetStatus }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({ error: res.statusText }));
console.error("Failed to mark series:", body.error);
}
router.refresh();
} catch (err) {
console.error("Failed to mark series:", err);
} finally {
setLoading(false);
}
};
return (
<button
onClick={handleClick}
disabled={loading}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-sm font-medium transition-colors disabled:opacity-50 ${
allRead
? "border-green-500/30 bg-green-500/10 text-green-600 hover:bg-green-500/20"
: "border-border bg-card text-muted-foreground hover:text-foreground hover:border-primary"
}`}
>
{loading ? (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
) : allRead ? (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
</svg>
{label}
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
</svg>
{label}
</>
)}
</button>
);
}