'use client' import {forwardRef, useContext, useEffect, useImperativeHandle, useState} from "react"; import System from "@/lib/models/System"; import {AlertContext} from "@/context/AlertContext"; import {SessionContext} from "@/context/SessionContext"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import {SeriesContext, SeriesContextProps} from "@/context/SeriesContext"; import {SeriesBookProps} from "@/lib/models/Series"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; import {SyncedBook} from "@/lib/models/SyncedBook"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faArrowDown, faArrowUp, faBook, faPlus, faSpinner, faTrash} from "@fortawesome/free-solid-svg-icons"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; import {SeriesSyncContext, SeriesSyncContextProps} from "@/context/SeriesSyncContext"; import {SyncedSeries, SyncedSeriesBook} from "@/lib/models/SyncedSeries"; function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Promise }>) { const t = useTranslations(); const {lang} = useContext(LangContext); const {session} = useContext(SessionContext); const {seriesId, localSeries} = useContext(SeriesContext); const {serverSyncedBooks, setServerSyncedBooks, localSyncedBooks} = useContext(BooksSyncContext); const userToken: string = session?.accessToken ? session?.accessToken : ''; const {errorMessage, successMessage} = useContext(AlertContext); const {isCurrentlyOffline} = useContext(OfflineContext); const {addToQueue} = useContext(LocalSyncQueueContext); const {localSyncedSeries, serverSyncedSeries} = useContext(SeriesSyncContext); const [isLoading, setIsLoading] = useState(true); const [seriesBooks, setSeriesBooks] = useState([]); const [selectedBookToAdd, setSelectedBookToAdd] = useState(''); const [availableBooks, setAvailableBooks] = useState([]); useEffect(function () { if (seriesId) { loadSeriesBooks(); } }, [seriesId]); useEffect(function () { const booksInThisSeries: string[] = seriesBooks.map((book: SeriesBookProps) => book.bookId); let allBooks: SyncedBook[]; let allSeries: SyncedSeries[]; if (isCurrentlyOffline() || localSeries) { allBooks = localSyncedBooks; allSeries = localSyncedSeries; } else { allBooks = serverSyncedBooks; allSeries = serverSyncedSeries; } // Get all bookIds in OTHER series (not this one) const booksInOtherSeries: Set = new Set( allSeries .filter((series: SyncedSeries): boolean => series.id !== seriesId) .flatMap((series: SyncedSeries): string[] => series.books.map((book: SyncedSeriesBook): string => book.bookId) ) ); // Filter out books already in this series AND books already in another series const filteredBooks: SyncedBook[] = allBooks.filter( (book: SyncedBook) => !booksInThisSeries.includes(book.id) && !booksInOtherSeries.has(book.id) ); setAvailableBooks(filteredBooks); }, [seriesBooks, serverSyncedBooks, localSyncedBooks, serverSyncedSeries, localSyncedSeries, isCurrentlyOffline, localSeries, seriesId]); async function loadSeriesBooks(): Promise { setIsLoading(true); try { let response: SeriesBookProps[]; if (isCurrentlyOffline() || localSeries) { response = await window.electron.invoke('db:series:books', {seriesId}); } else { response = await System.authGetQueryToServer( 'series/book/list', userToken, lang, {seriesid: seriesId} ); } if (response) { setSeriesBooks(response); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesBooks.error.unknown')); } } finally { setIsLoading(false); } } useImperativeHandle(ref, function () { return { handleSave: handleSave }; }); async function handleSave(): Promise { successMessage(t('seriesBooks.success.saved')); } async function handleAddBook(): Promise { if (!selectedBookToAdd) { errorMessage(t('seriesBooks.error.selectBook')); return; } try { const addData = { seriesId: seriesId, bookId: selectedBookToAdd }; let response: boolean; if (isCurrentlyOffline() || localSeries) { response = await window.electron.invoke('db:series:book:add', addData); } else { response = await System.authPostToServer( 'series/book/add', addData, userToken, lang ); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) { addToQueue('db:series:book:add', addData); } } if (response) { const allBooks: SyncedBook[] = isCurrentlyOffline() || localSeries ? localSyncedBooks : serverSyncedBooks; const addedBook: SyncedBook | undefined = allBooks.find( (book: SyncedBook) => book.id === selectedBookToAdd ); if (addedBook) { const newSeriesBook: SeriesBookProps = { bookId: addedBook.id, title: addedBook.title, order: seriesBooks.length + 1, coverImage: null }; setSeriesBooks([...seriesBooks, newSeriesBook]); setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => prev.map((book: SyncedBook): SyncedBook => book.id === selectedBookToAdd ? {...book, seriesId: seriesId} : book ) ); } setSelectedBookToAdd(''); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesBooks.error.unknown')); } } } async function handleRemoveBook(bookId: string): Promise { try { const removeData = { seriesId: seriesId, bookId: bookId }; let response: boolean; if (isCurrentlyOffline() || localSeries) { response = await window.electron.invoke('db:series:book:remove', removeData); } else { response = await System.authDeleteToServer( 'series/book/remove', removeData, userToken, lang ); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) { addToQueue('db:series:book:remove', removeData); } } if (response) { const updatedBooks: SeriesBookProps[] = seriesBooks .filter((book: SeriesBookProps) => book.bookId !== bookId) .map((book: SeriesBookProps, index: number) => ({ ...book, order: index + 1 })); setSeriesBooks(updatedBooks); setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => prev.map((book: SyncedBook): SyncedBook => book.id === bookId ? {...book, seriesId: null} : book ) ); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesBooks.error.unknown')); } } } async function handleMoveBook(bookId: string, direction: 'up' | 'down'): Promise { const currentIndex: number = seriesBooks.findIndex((book: SeriesBookProps) => book.bookId === bookId); if (currentIndex === -1) return; const newIndex: number = direction === 'up' ? currentIndex - 1 : currentIndex + 1; if (newIndex < 0 || newIndex >= seriesBooks.length) return; const reorderedBooks: SeriesBookProps[] = [...seriesBooks]; const [movedBook] = reorderedBooks.splice(currentIndex, 1); reorderedBooks.splice(newIndex, 0, movedBook); const updatedBooks: SeriesBookProps[] = reorderedBooks.map((book: SeriesBookProps, index: number) => ({ ...book, order: index + 1 })); try { const reorderData = { seriesId: seriesId, booksOrder: updatedBooks.map((book: SeriesBookProps) => ({ bookId: book.bookId, order: book.order })) }; let response: boolean; if (isCurrentlyOffline() || localSeries) { response = await window.electron.invoke('db:series:book:reorder', reorderData); } else { response = await System.authPutToServer( 'series/book/reorder', reorderData, userToken, lang ); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) { addToQueue('db:series:book:reorder', reorderData); } } if (response) { setSeriesBooks(updatedBooks); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesBooks.error.unknown')); } } } if (isLoading) { return (
); } return (

{t('seriesBooks.addBook')}

{t('seriesBooks.booksInSeries')} ({seriesBooks.length})

{seriesBooks.length === 0 ? (

{t('seriesBooks.noBooks')}

) : (
{seriesBooks .sort((a: SeriesBookProps, b: SeriesBookProps) => a.order - b.order) .map((book: SeriesBookProps, index: number) => (
{book.order} {book.title}
))}
)}
); } export default forwardRef(SeriesBooksManager);