mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-05 13:50:11 +00:00
Add Hardcover shelf sync & unify book mappings
Introduce Hardcover provider support and consolidate per-provider book mapping tables into a unified BookMapping model. Adds two Prisma migrations (add_hardcover_shelves, unify_book_mappings), new backend services (hardcover-api, shelf-sync-core), and provider-specific sync logic and API routes for hardcover shelves with token/list validation. Frontend: new HardcoverForm component, refactor AddShelfModal to support Hardcover, hook updates, and small UI/accessibility tweaks. Also add documentation for Goodreads and Hardcover sync flows and update tests to cover scheduler/prisma helpers.
This commit is contained in:
@@ -5,17 +5,9 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { fetchWithAuth } from '@/lib/utils/api';
|
||||
import { createShelfHooks, ShelfBook } from './createShelfHooks';
|
||||
|
||||
export interface ShelfBook {
|
||||
coverUrl: string;
|
||||
asin: string | null;
|
||||
title: string;
|
||||
author: string;
|
||||
}
|
||||
export type { ShelfBook };
|
||||
|
||||
export interface HardcoverShelf {
|
||||
id: string;
|
||||
@@ -27,161 +19,31 @@ export interface HardcoverShelf {
|
||||
books: ShelfBook[];
|
||||
}
|
||||
|
||||
const fetcher = (url: string) => fetchWithAuth(url).then((res) => res.json());
|
||||
const { useList, useAdd, useDelete, useUpdate } =
|
||||
createShelfHooks<HardcoverShelf>('/api/user/hardcover-shelves');
|
||||
|
||||
export function useHardcoverShelves() {
|
||||
const { accessToken } = useAuth();
|
||||
|
||||
const endpoint = accessToken ? '/api/user/hardcover-shelves' : null;
|
||||
|
||||
const { data, error, isLoading } = useSWR(endpoint, fetcher, {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
return {
|
||||
shelves: (data?.shelves || []) as HardcoverShelf[],
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
export const useHardcoverShelves = useList;
|
||||
|
||||
export function useAddHardcoverShelf() {
|
||||
const { accessToken } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { addShelf: addGeneric, isLoading, error } = useAdd();
|
||||
|
||||
const addShelf = async (apiToken: string, listId: string) => {
|
||||
if (!accessToken) throw new Error('Not authenticated');
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/api/user/hardcover-shelves', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apiToken, listId }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to add list');
|
||||
}
|
||||
|
||||
// Revalidate shelves list
|
||||
mutate(
|
||||
(key) =>
|
||||
typeof key === 'string' &&
|
||||
key.includes('/api/user/hardcover-shelves'),
|
||||
);
|
||||
|
||||
return data.shelf as HardcoverShelf;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
return addGeneric({ apiToken, listId });
|
||||
};
|
||||
|
||||
return { addShelf, isLoading, error };
|
||||
}
|
||||
|
||||
export function useDeleteHardcoverShelf() {
|
||||
const { accessToken } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const deleteShelf = async (shelfId: string) => {
|
||||
if (!accessToken) throw new Error('Not authenticated');
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(
|
||||
`/api/user/hardcover-shelves/${shelfId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to remove list');
|
||||
}
|
||||
|
||||
// Revalidate shelves list
|
||||
mutate(
|
||||
(key) =>
|
||||
typeof key === 'string' &&
|
||||
key.includes('/api/user/hardcover-shelves'),
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { deleteShelf, isLoading, error };
|
||||
}
|
||||
export const useDeleteHardcoverShelf = useDelete;
|
||||
|
||||
export function useUpdateHardcoverShelf() {
|
||||
const { accessToken } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { updateShelf: updateGeneric, isLoading, error } = useUpdate();
|
||||
|
||||
const updateShelf = async (
|
||||
shelfId: string,
|
||||
updates: { listId?: string; apiToken?: string },
|
||||
) => {
|
||||
if (!accessToken) throw new Error('Not authenticated');
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(
|
||||
`/api/user/hardcover-shelves/${shelfId}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to update list');
|
||||
}
|
||||
|
||||
// Revalidate shelves list
|
||||
mutate(
|
||||
(key) =>
|
||||
typeof key === 'string' &&
|
||||
key.includes('/api/user/hardcover-shelves'),
|
||||
);
|
||||
mutate(
|
||||
(key) => typeof key === 'string' && key.includes('/api/user/shelves'),
|
||||
);
|
||||
|
||||
return data.shelf as HardcoverShelf;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
return updateGeneric(shelfId, updates);
|
||||
};
|
||||
|
||||
return { updateShelf, isLoading, error };
|
||||
|
||||
Reference in New Issue
Block a user