'use client' import {forwardRef, useContext, useEffect, useImperativeHandle, useState} from "react"; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import {isDesktop} from '@/lib/configs'; import {apiDelete, apiGet, apiPost, apiPut} from '@/lib/api/client'; import * as tauri from '@/lib/tauri'; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {useTranslations} from '@/lib/i18n'; import {LangContext, LangContextProps} from "@/context/LangContext"; import {SeriesContext, SeriesContextProps} from "@/context/SeriesContext"; import {SeriesBookProps} from "@/lib/types/series"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; import {SyncedBook} from "@/lib/types/synced-book"; import {ArrowDown, ArrowUp, Book, Trash2} from 'lucide-react'; import PulseLoader from '@/components/ui/PulseLoader'; import InputField from '@/components/form/InputField'; import SelectBox from '@/components/form/SelectBox'; import IconButton from "@/components/ui/IconButton"; import EmptyState from "@/components/ui/EmptyState"; import Badge from "@/components/ui/Badge"; import EntityListItem from "@/components/ui/EntityListItem"; function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Promise }>) { const t = useTranslations(); const {lang}: LangContextProps = useContext(LangContext); const {session}: SessionContextProps = useContext(SessionContext); const {seriesId}: SeriesContextProps = useContext(SeriesContext); const { serverSyncedBooks, setServerSyncedBooks, localSyncedBooks }: BooksSyncContextProps = useContext(BooksSyncContext); const userToken: string = session?.accessToken ? session?.accessToken : ''; const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {isCurrentlyOffline} = useContext(OfflineContext); const useLocal: boolean = isDesktop && isCurrentlyOffline(); 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 booksInSeries: string[] = seriesBooks.map((book: SeriesBookProps) => book.bookId); const allBooks: SyncedBook[] = useLocal ? localSyncedBooks : serverSyncedBooks; const filteredBooks: SyncedBook[] = allBooks.filter( (book: SyncedBook) => !booksInSeries.includes(book.id) ); setAvailableBooks(filteredBooks); }, [seriesBooks, serverSyncedBooks, localSyncedBooks, useLocal]); async function loadSeriesBooks(): Promise { setIsLoading(true); try { const response: SeriesBookProps[] = useLocal ? await tauri.getSeriesBooks(seriesId) as SeriesBookProps[] : await apiGet('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 response: boolean = useLocal ? await tauri.addBookToSeries(seriesId, selectedBookToAdd) : await apiPost('series/book/add', {seriesId: seriesId, bookId: selectedBookToAdd}, userToken, lang); if (response) { const allBooks: SyncedBook[] = useLocal ? 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 deletedAt: number = Math.floor(Date.now() / 1000); const response: boolean = useLocal ? await tauri.removeBookFromSeries(seriesId, bookId, deletedAt) : await apiDelete('series/book/remove', {seriesId: seriesId, bookId: bookId}, userToken, lang); 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 bookIds: string[] = updatedBooks.map((book: SeriesBookProps) => book.bookId); const response: boolean = useLocal ? await tauri.reorderSeriesBooks(seriesId, bookIds) : await apiPut('series/book/reorder', {seriesId: seriesId, booksOrder: updatedBooks.map((book: SeriesBookProps) => ({bookId: book.bookId, order: book.order}))}, userToken, lang); if (response) { setSeriesBooks(updatedBooks); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('seriesBooks.error.unknown')); } } } if (isLoading) { return ; } return (
setSelectedBookToAdd(e.target.value)} data={availableBooks.map((book: SyncedBook) => ({label: book.title, value: book.id}))} defaultValue={selectedBookToAdd} placeholder={t('seriesBooks.selectBookPlaceholder')} /> } addButtonCallBack={handleAddBook} isAddButtonDisabled={!selectedBookToAdd} />

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

{seriesBooks.length === 0 ? ( ) : (
{seriesBooks .sort((a: SeriesBookProps, b: SeriesBookProps) => a.order - b.order) .map((book: SeriesBookProps, index: number) => ( {book.order}} title={book.title} extra={
handleMoveBook(book.bookId, 'up')} disabled={index === 0} tooltip={t('seriesBooks.moveUp')} /> handleMoveBook(book.bookId, 'down')} disabled={index === seriesBooks.length - 1} tooltip={t('seriesBooks.moveDown')} /> handleRemoveBook(book.bookId)} tooltip={t('seriesBooks.removeBook')} />
} /> ))}
)}
); } export default forwardRef(SeriesBooksManager);