diff --git a/src/lib/books/series.ts b/src/lib/books/series.ts new file mode 100644 index 0000000..53fc18e --- /dev/null +++ b/src/lib/books/series.ts @@ -0,0 +1,60 @@ +import type { Book, BookShelf } from '#lib/content'; + +export interface BookSeriesGroup { + name: string; + url: string; + books: Book[]; + counts: Record; +} + +export interface GroupedBooks { + series: BookSeriesGroup[]; + standalone: Book[]; +} + +const positionGroups = { single: 0, range: 1, none: 2 }; + +function seriesOrder(first: Book, second: Book): number { + const [firstGroup, firstNumber] = positionKey(first); + const [secondGroup, secondNumber] = positionKey(second); + return firstGroup - secondGroup || firstNumber - secondNumber; +} + +function positionKey(book: Book): [number, number] { + const position = book.series?.position ?? ''; + const match = position.match(/\d+(?:\.\d+)?/); + if (!match) return [positionGroups.none, 0]; + const isRange = /\d\s*[-–]\s*\d/.test(position); + return [isRange ? positionGroups.range : positionGroups.single, Number(match[0])]; +} + +function shelfCounts(books: Book[]): Record { + return { + read: books.filter((book) => book.shelf === 'read').length, + reading: books.filter((book) => book.shelf === 'reading').length, + 'want-to-read': books.filter((book) => book.shelf === 'want-to-read').length + }; +} + +export function groupBySeries(books: Book[]): GroupedBooks { + const groups = new Map(); + const standalone: Book[] = []; + + for (const book of books) { + if (!book.series) { + standalone.push(book); + continue; + } + const key = book.series.url || book.series.name; + groups.set(key, [...(groups.get(key) ?? []), book]); + } + + const series = [...groups.values()] + .map((seriesBooks) => { + const ordered = [...seriesBooks].sort(seriesOrder); + return { name: ordered[0].series!.name, url: ordered[0].series!.url, books: ordered, counts: shelfCounts(ordered) }; + }) + .sort((first, second) => second.counts.read - first.counts.read || first.name.localeCompare(second.name)); + + return { series, standalone }; +} diff --git a/src/lib/components/about/books/BookCard.svelte b/src/lib/components/about/books/BookCard.svelte index ca708c6..65e48ac 100644 --- a/src/lib/components/about/books/BookCard.svelte +++ b/src/lib/components/about/books/BookCard.svelte @@ -2,7 +2,9 @@ import Icon from '@iconify/svelte'; import type { Book } from '#lib/content'; - let { book }: { book: Book } = $props(); + let { book, showShelf = false }: { book: Book; showShelf?: boolean } = $props(); + + const shelfLabels = { read: 'read', reading: 'reading', 'want-to-read': 'want to read' }; let coverFailed = $state(false); @@ -26,14 +28,22 @@ {book.averageRating} {/if} + {#if showShelf} + {shelfLabels[book.shelf]} + {/if} + {#if book.description}

{book.description}

{/if} {book.title} - {book.authors.join(', ')} - {#if book.genres.length} + {#if book.series?.position} + {book.series.position} + {:else} + {book.authors.join(', ')} + {/if} + {#if book.genres.length && !showShelf} {book.genres.slice(0, 3).join(' · ')} {/if} @@ -104,6 +114,36 @@ color: var(--muted); } + .shelf { + position: absolute; + bottom: 0.35rem; + left: 0.35rem; + padding: 0 0.35rem; + color: var(--background); + font-size: 0.7rem; + font-weight: 700; + } + + .shelf.read { + background: var(--green); + } + + .shelf.reading { + background: var(--blue); + } + + .shelf.want-to-read { + background: var(--magenta); + } + + .book-card:hover .shelf { + opacity: 0; + } + + .position { + color: var(--foreground); + } + .description { position: absolute; right: 0; diff --git a/src/lib/components/about/books/BookSeriesGroup.svelte b/src/lib/components/about/books/BookSeriesGroup.svelte new file mode 100644 index 0000000..542aec9 --- /dev/null +++ b/src/lib/components/about/books/BookSeriesGroup.svelte @@ -0,0 +1,152 @@ + + +
+
+ + + {series.name} + + + {#each shelves.filter((shelf) => shelf.count > 0) as shelf (shelf.id)} + {shelf.count} {shelf.label} + {/each} + +
+ + + +
+ {#each shownBooks as book (book.goodreadsId)} + + {/each} +
+ + {#if total > collapsedCount} + + {/if} +
+ + diff --git a/src/lib/components/about/books/BooksSection.svelte b/src/lib/components/about/books/BooksSection.svelte index eeaa477..e1febb2 100644 --- a/src/lib/components/about/books/BooksSection.svelte +++ b/src/lib/components/about/books/BooksSection.svelte @@ -1,8 +1,10 @@