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); }