import { invalidate } from '$app/navigation'; import { addBooksToShelf, createBookshelf, listBookshelves, removeBooksFromShelf } from '$lib/api'; import type { Book, Bookshelf } from '$lib/schema'; import { getContext, setContext } from 'svelte'; import { toast } from 'svelte-sonner'; import { SvelteMap } from 'svelte/reactivity'; export class BookshelfState { readonly libraryBookshelves = new SvelteMap(); getBookshelves(libraryId: string | number): Bookshelf[] | undefined { const id = libraryId.toString(); if (!this.libraryBookshelves.has(id)) { this.fetchBookshelves(id).then((shelves) => { this.libraryBookshelves.set(id, shelves); }); } return this.libraryBookshelves.get(id); } async fetchBookshelves(libraryId: string) { try { const paginatedBookshelves = await listBookshelves({ libraries: [libraryId] }); return paginatedBookshelves.items; } catch (error) { console.error('Failed to retrieve bookshelves: ', error); } } async addBookshelf(name: string, libraryId?: string | number, booksToAdd?: string[] | number[]) { try { const bookshelf = await createBookshelf({ title: name, library_id: libraryId, book_ids: booksToAdd }); if (bookshelf.library_id) { const currentShelves = this.libraryBookshelves.get(bookshelf.library_id.toString()) || []; this.libraryBookshelves.set(bookshelf.library_id.toString(), [ ...currentShelves, bookshelf ]); } invalidate('app:books'); if (booksToAdd?.length) toast.success(`Added ${booksToAdd.length} books to '${name}'`); else toast.success(`Created shelf '${name}'`); return bookshelf; } catch (error) { toast.error(`Failed to create bookshelf '${name}'`); console.error(`Failed to create bookshelf: `, error); } } async addBooksToShelf(shelfId: number | string, bookIds: number[]) { try { const shelf = await addBooksToShelf({ shelf_id: shelfId, book_ids: bookIds }); this.updateShelf(shelf); if (bookIds.length === 1) toast.success(`Added book to shelf!`); else toast.success(`Added ${bookIds.length} books to shelf!`); } catch (error) { console.error('Failed to add book(s) to shelf: ', error); toast.error('Failed to add book(s) to shelf.'); } } async removeBooksFromShelf(shelfId: number | string, bookIds: number[]) { try { const shelf = await removeBooksFromShelf({ shelf_id: shelfId, book_ids: bookIds }); this.updateShelf(shelf); if (bookIds.length === 1) toast.success(`Removed book from shelf.`); else toast.success(`Removed ${bookIds.length} books from shelf!`); } catch (error) { console.error('Failed to remove book(s) from shelf: ', error); toast.error('Failed to remove book(s) from shelf.'); } } async updateShelf(shelf: Bookshelf) { if (!shelf.library_id) return; const bookshelves = this.libraryBookshelves.get(shelf.library_id.toString()); if (!bookshelves) return; const index = bookshelves.findIndex((bookshelf) => bookshelf.id === shelf.id); if (index !== -1) { this.libraryBookshelves.set(shelf.library_id.toString(), [ ...bookshelves.slice(0, index), shelf, ...bookshelves.slice(index + 1) ]); } } deletedBooks(books: Book[]) { // Assume all books are in the same library const libraryId = books[0].library_id.toString(); const bookshelves = this.libraryBookshelves.get(libraryId); if (!bookshelves) return; // Create a Set of shelf IDs that need updating for efficient lookup // eslint-disable-next-line svelte/prefer-svelte-reactivity -- local temporary, not reactive state const shelfIdsToUpdate = new Set(); books.forEach((book) => { book.lists.forEach((shelf) => { shelfIdsToUpdate.add(shelf.id); }); }); const updatedBookshelves = bookshelves.map((shelf) => { if (shelfIdsToUpdate.has(shelf.id)) { // Count how many times this shelf appears across all deleted books let decrementBy = 0; books.forEach((book) => { if (book.lists.some((s) => s.id === shelf.id)) { decrementBy++; } }); return { ...shelf, total: (shelf.total ?? 0) - decrementBy }; } return shelf; }); this.libraryBookshelves.set(libraryId, updatedBookshelves); } } const BOOKSHELF_KEY = Symbol('BOOKSHELF'); export function setBookshelfState() { return setContext(BOOKSHELF_KEY, new BookshelfState()); } export function getBookshelfState() { return getContext>(BOOKSHELF_KEY); }