mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-04 13:20:11 +00:00
09cff5b68d
Introduce a per-user "ignored audiobooks" feature to suppress auto-requests. Changes include: - Database: add Prisma model IgnoredAudiobook and SQL migration to create ignored_audiobooks table with indexes and FK to users. - Backend: new API routes to list, add, delete, and check ignored audiobooks (/api/user/ignored-audiobooks, /check/:asin, /:id). Add annotateWithIgnoreStatus utility and integrate it into multiple audiobook list endpoints (popular, new-releases, category, search, authors, series). - Request creator: add ignore-list check (with sibling-ASIN expansion) and a bypassIgnore option for manual requests; return an 'ignored' reason when blocked. - Frontend: hooks (useIsIgnored, useToggleIgnore, useIgnoredList) and UI updates — AudiobookCard shows an "Ignored" indicator and AudiobookDetailsModal adds an ignore toggle and propagates local state changes. - Misc: adjust deduplication duration tolerance (to 5% / min 10 minutes), tweak SWR refresh intervals for shelves/syncing, and small logging/info updates. - Tests: add unit tests for request-creator ignore logic and update existing tests/mocks to account for ignore annotation; extend prisma test helper with ignoredAudiobook mock. This commit implements the ignore-list end-to-end (DB, server, client, and tests) so users can ignore specific ASINs and have auto-request flows respect that preference.
124 lines
3.1 KiB
TypeScript
124 lines
3.1 KiB
TypeScript
/**
|
|
* Component: Ignored Audiobooks Hook
|
|
* Documentation: documentation/features/ignored-audiobooks.md
|
|
*
|
|
* Provides hooks for checking and toggling audiobook ignore status.
|
|
* - useIsIgnored(asin): check if a specific book is ignored
|
|
* - useToggleIgnore(): toggle ignore on/off for a book
|
|
* - useIgnoredList(): list all ignored books for the current user
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import useSWR, { mutate } from 'swr';
|
|
import { authenticatedFetcher, fetchWithAuth } from '@/lib/utils/api';
|
|
|
|
interface IgnoredAudiobook {
|
|
id: string;
|
|
asin: string;
|
|
title: string;
|
|
author: string;
|
|
coverArtUrl?: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface IgnoreCheckResult {
|
|
ignored: boolean;
|
|
ignoredId?: string;
|
|
}
|
|
|
|
/**
|
|
* Check if a specific ASIN is ignored by the current user.
|
|
* Includes works-system expansion on the server side.
|
|
*/
|
|
export function useIsIgnored(asin: string | null) {
|
|
const endpoint = asin ? `/api/user/ignored-audiobooks/check/${asin}` : null;
|
|
|
|
const { data, error, isLoading } = useSWR<IgnoreCheckResult>(
|
|
endpoint,
|
|
authenticatedFetcher,
|
|
{
|
|
revalidateOnFocus: false,
|
|
dedupingInterval: 30000,
|
|
}
|
|
);
|
|
|
|
return {
|
|
isIgnored: data?.ignored ?? false,
|
|
ignoredId: data?.ignoredId ?? null,
|
|
isLoading,
|
|
error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Toggle ignore status for an audiobook.
|
|
* Returns { addIgnore, removeIgnore } functions.
|
|
*/
|
|
export function useToggleIgnore() {
|
|
const addIgnore = async (book: {
|
|
asin: string;
|
|
title: string;
|
|
author: string;
|
|
coverArtUrl?: string;
|
|
}): Promise<IgnoredAudiobook> => {
|
|
const res = await fetchWithAuth('/api/user/ignored-audiobooks', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(book),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.message || 'Failed to ignore audiobook');
|
|
}
|
|
|
|
const result = await res.json();
|
|
|
|
// Invalidate the check cache for this ASIN
|
|
mutate(`/api/user/ignored-audiobooks/check/${book.asin}`);
|
|
// Invalidate the full list
|
|
mutate('/api/user/ignored-audiobooks');
|
|
|
|
return result.ignoredAudiobook;
|
|
};
|
|
|
|
const removeIgnore = async (id: string, asin: string): Promise<void> => {
|
|
const res = await fetchWithAuth(`/api/user/ignored-audiobooks/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.message || 'Failed to un-ignore audiobook');
|
|
}
|
|
|
|
// Invalidate the check cache for this ASIN
|
|
mutate(`/api/user/ignored-audiobooks/check/${asin}`);
|
|
// Invalidate the full list
|
|
mutate('/api/user/ignored-audiobooks');
|
|
};
|
|
|
|
return { addIgnore, removeIgnore };
|
|
}
|
|
|
|
/**
|
|
* List all ignored audiobooks for the current user.
|
|
*/
|
|
export function useIgnoredList() {
|
|
const { data, error, isLoading } = useSWR<{ ignoredAudiobooks: IgnoredAudiobook[] }>(
|
|
'/api/user/ignored-audiobooks',
|
|
authenticatedFetcher,
|
|
{
|
|
revalidateOnFocus: false,
|
|
dedupingInterval: 60000,
|
|
}
|
|
);
|
|
|
|
return {
|
|
ignoredAudiobooks: data?.ignoredAudiobooks ?? [],
|
|
isLoading,
|
|
error,
|
|
};
|
|
}
|