Spaces:
Running
Running
| export const SAVED_BOOKS_STORAGE_KEY = "world-citybook-saved-books"; | |
| export function normalizeSavedBookRecords(value) { | |
| const items = Array.isArray(value) ? value : []; | |
| const records = []; | |
| const byId = new Map(); | |
| items.forEach((item) => { | |
| const bookId = typeof item === "string" ? item.trim() : String(item?.bookId || "").trim(); | |
| if (!bookId) return; | |
| const savedAt = typeof item === "object" && item?.savedAt ? String(item.savedAt) : ""; | |
| if (byId.has(bookId)) { | |
| const existing = byId.get(bookId); | |
| existing.savedAt = latestTimestamp(existing.savedAt, savedAt); | |
| return; | |
| } | |
| const record = { bookId, savedAt }; | |
| byId.set(bookId, record); | |
| records.push(record); | |
| }); | |
| return records; | |
| } | |
| export function savedBookIds(records) { | |
| return normalizeSavedBookRecords(records).map((record) => record.bookId); | |
| } | |
| export function paginateShelfItems(items, options = {}) { | |
| const source = Array.isArray(items) ? items : []; | |
| const pageSize = Math.max(1, Math.floor(Number(options.pageSize) || 1)); | |
| const totalPages = Math.max(1, Math.ceil(source.length / pageSize)); | |
| const page = clampInteger(options.page, 0, totalPages - 1); | |
| const start = page * pageSize; | |
| return { | |
| page, | |
| pageSize, | |
| totalPages, | |
| items: source.slice(start, start + pageSize), | |
| }; | |
| } | |
| export function buildFixedShelfRows(items, options = {}) { | |
| const source = Array.isArray(items) ? items : []; | |
| const rowSize = Math.max(1, Math.floor(Number(options.rowSize) || 1)); | |
| const rowCount = Math.max(1, Math.floor(Number(options.rowCount) || 1)); | |
| return Array.from({ length: rowCount }, (_, rowIndex) => { | |
| const start = rowIndex * rowSize; | |
| return source.slice(start, start + rowSize); | |
| }); | |
| } | |
| export function upsertSavedBook(records, bookId, savedAt = new Date().toISOString()) { | |
| const normalizedBookId = String(bookId || "").trim(); | |
| if (!normalizedBookId) return normalizeSavedBookRecords(records); | |
| const next = normalizeSavedBookRecords(records).filter((record) => record.bookId !== normalizedBookId); | |
| return [{ bookId: normalizedBookId, savedAt }, ...next]; | |
| } | |
| export function removeSavedBook(records, bookId) { | |
| const normalizedBookId = String(bookId || "").trim(); | |
| return normalizeSavedBookRecords(records).filter((record) => record.bookId !== normalizedBookId); | |
| } | |
| function latestTimestamp(left, right) { | |
| if (!left) return right || ""; | |
| if (!right) return left; | |
| return right > left ? right : left; | |
| } | |
| function clampInteger(value, min, max) { | |
| const number = Math.floor(Number(value) || 0); | |
| return Math.min(Math.max(number, min), max); | |
| } | |