mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 21:00:09 +00:00
Add watched series/authors feature
Introduce watched lists for series and authors end-to-end. - Add DB migration to create watched_series and watched_authors tables with indexes and foreign keys. - Implement API routes: GET/POST for listing/adding and DELETE by id for both /api/user/watched-series and /api/user/watched-authors. Validation, ownership checks, and immediate targeted job triggers are included. - Add client hooks (useWatchedSeries, useWatchedAuthors) with add/delete helpers and SWR revalidation. - Add UI components: WatchButton (toggle/confirm) and WatchedListsSection for profile display and removal UX. - Add processor (check-watched-lists.processor) and service (watched-lists.service) to scrape Audible, deduplicate, check library ownership, and auto-create requests; supports targeted checks for newly watched items. - Include tests for the watched-lists service. These changes implement the watched-lists feature to let users watch series/authors and have the system automatically detect and request new releases.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Component: Watched Authors Hook
|
||||
* Documentation: documentation/features/watched-lists.md
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { fetchWithAuth } from '@/lib/utils/api';
|
||||
|
||||
export interface WatchedAuthorItem {
|
||||
id: string;
|
||||
authorAsin: string;
|
||||
authorName: string;
|
||||
coverArtUrl: string | null;
|
||||
lastCheckedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const fetcher = (url: string) =>
|
||||
fetchWithAuth(url).then((res) => res.json());
|
||||
|
||||
export function useWatchedAuthors() {
|
||||
const { accessToken } = useAuth();
|
||||
|
||||
const endpoint = accessToken ? '/api/user/watched-authors' : null;
|
||||
|
||||
const { data, error, isLoading } = useSWR(
|
||||
endpoint,
|
||||
fetcher,
|
||||
{ refreshInterval: 60000 }
|
||||
);
|
||||
|
||||
return {
|
||||
authors: (data?.authors || []) as WatchedAuthorItem[],
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAddWatchedAuthor() {
|
||||
const { accessToken } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const addAuthor = async (authorAsin: string, authorName: string, coverArtUrl?: string) => {
|
||||
if (!accessToken) throw new Error('Not authenticated');
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/user/watched-authors', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ authorAsin, authorName, coverArtUrl }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to watch author');
|
||||
}
|
||||
|
||||
// Revalidate watched authors list
|
||||
mutate((key) => typeof key === 'string' && key.includes('/api/user/watched-authors'));
|
||||
|
||||
return data.author as WatchedAuthorItem;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { addAuthor, isLoading, error };
|
||||
}
|
||||
|
||||
export function useDeleteWatchedAuthor() {
|
||||
const { accessToken } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const deleteAuthor = async (id: string) => {
|
||||
if (!accessToken) throw new Error('Not authenticated');
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`/api/user/watched-authors/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to unwatch author');
|
||||
}
|
||||
|
||||
// Revalidate watched authors list
|
||||
mutate((key) => typeof key === 'string' && key.includes('/api/user/watched-authors'));
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { deleteAuthor, isLoading, error };
|
||||
}
|
||||
Reference in New Issue
Block a user