Initial commit

This commit is contained in:
hiperman
2025-12-04 00:33:37 -05:00
commit 7ca0a21283
798 changed files with 190424 additions and 0 deletions
@@ -0,0 +1,229 @@
import { getContext, setContext } from 'svelte';
import { goto, pushState, replaceState } from '$app/navigation';
import type { Book, DynamicFilterData, FilterOption, PaginatedResponse } from '$lib/schema';
import { page } from '$app/state';
import { BookOperationsState } from './bookOperations.svelte';
export class BookCollectionState {
public sortOrder = $state<string>('');
public orderBy = $state<string>('');
public filters = $state<Record<string, string[]>>({});
readonly hasActiveSort = $derived(this.orderBy !== 'title' || this.sortOrder !== 'asc');
readonly hasActiveFilters = $derived(Object.values(this.filters).some((arr) => arr.length > 0));
public books = $state<Book[]>([]);
public moreBooks = $state(false);
private currentBookPage = $state(1);
public loading = $state(false);
public filterOptions: FilterOption[] = $state([]);
private ops: BookOperationsState;
readonly sortOptions = [
{
value: 'title',
name: 'Title'
},
{
value: 'created_at',
name: 'Date Added'
},
{
value: 'published_date',
name: 'Date Published'
},
{
value: 'pages',
name: 'Pages'
},
{
value: 'last_accessed',
name: 'Last Accessed'
}
];
constructor(
ops: BookOperationsState,
books: PaginatedResponse<Book>,
filterData: DynamicFilterData
) {
this.ops = ops;
this.books = books.items;
this.moreBooks = books.total > books.items.length;
this.filterOptions = this.buildFilterOptions(filterData);
// Determine sort order and direction based on page data
this.orderBy = page.url.searchParams.get('orderBy') || 'title';
this.sortOrder = page.url.searchParams.get('sortOrder') || 'asc';
// Construct initial filters based on the page data
this.filters = {
authors: page.url.searchParams.getAll('authors'),
publishers: page.url.searchParams.getAll('publishers'),
progress: page.url.searchParams.getAll('progress'),
tags: page.url.searchParams.getAll('tags'),
shelves: page.url.searchParams.getAll('shelves')
};
}
updateSort(sortValue: string) {
if (sortValue === this.orderBy) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.orderBy = sortValue;
}
this.updateSearchParams();
}
toggleFilter(filter: string, value: string) {
if (!this.filters[filter]) {
this.filters[filter] = [];
}
const index = this.filters[filter].indexOf(value);
if (index > -1) {
this.filters[filter].splice(index, 1);
} else {
this.filters[filter].push(value);
}
this.updateSearchParams();
}
updateSearchParams() {
// Update URL
const url = new URL(window.location.href);
Object.entries(this.filters).forEach(([filter, values]) => {
url.searchParams.delete(filter);
values.forEach((val) => url.searchParams.append(filter, val.toString()));
});
url.searchParams.set('orderBy', this.orderBy);
url.searchParams.set('sortOrder', this.sortOrder);
// pushState(url.toString(), {})
goto(url.toString());
this.loadNewBooks();
}
async loadNewBooks() {
this.loading = true;
const result = await this.ops.listBooks({
currentPage: 1,
pageSize: 50,
sortOrder: this.sortOrder,
orderBy: this.orderBy,
...this.filters
});
this.books = [...result.items];
this.currentBookPage = 1;
this.loading = false;
}
async loadMoreBooks() {
const result = await this.ops.listBooks({
currentPage: this.currentBookPage + 1,
pageSize: 50,
sortOrder: this.sortOrder,
orderBy: this.orderBy,
...this.filters
});
this.books = [...this.books, ...result.items];
this.moreBooks = result.total > this.books.length;
this.currentBookPage++;
}
updateBooks(books: PaginatedResponse<Book>) {
this.books = books.items;
this.moreBooks = books.total > books.items.length;
}
resetState(books: PaginatedResponse<Book>, filterData: DynamicFilterData) {
this.books = books.items;
this.moreBooks = books.total > books.items.length;
this.filterOptions = this.buildFilterOptions(filterData);
const urlParams = new URLSearchParams(window.location.search);
// Re-read sort and filter state from URL
this.orderBy = urlParams.get('orderBy') || 'title';
this.sortOrder = urlParams.get('sortOrder') || 'asc';
this.filters = {
authors: urlParams.getAll('authors'),
publishers: urlParams.getAll('publishers'),
progress: urlParams.getAll('progress'),
tags: urlParams.getAll('tags'),
shelves: urlParams.getAll('shelves')
};
}
buildFilterOptions(data: DynamicFilterData): FilterOption[] {
return [
{
value: 'shelves',
name: 'Bookshelves',
items: data.bookshelves.items
},
{
value: 'authors',
name: 'Authors',
items: data.authors.items
},
{
value: 'tags',
name: 'Tags',
items: data.tags.items
},
{
value: 'publishers',
name: 'Publishers',
items: data.publishers.items
},
{
value: 'progress',
name: 'Progress',
items: [
{ id: 'read', name: 'Read' },
{ id: 'in_progress', name: 'In Progress' },
{ id: 'unread', name: 'Not Started' }
]
}
];
}
updateFilterOptions(
filterOptions: { value: string; name: string; items: { name: string; id: number | string }[] }[]
) {
this.filterOptions = filterOptions;
}
clearFilters() {
Object.values(this.filters).forEach((val) => (val.length = 0));
this.updateSearchParams();
}
isFilterSelected(filter: string, value: string) {
return this.filters[filter]?.includes(value) || false;
}
}
const BOOK_COLLECTION_KEY = Symbol('BOOK_COLLECTION');
export function setBookCollectionState(
ops: BookOperationsState,
books: PaginatedResponse<Book>,
filterData: DynamicFilterData
) {
return setContext(BOOK_COLLECTION_KEY, new BookCollectionState(ops, books, filterData));
}
export function getBookCollectionState() {
return getContext<ReturnType<typeof setBookCollectionState>>(BOOK_COLLECTION_KEY);
}