feat: personal library — filter by read/saved books, add save button
Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 1s
Deploy / Cleanup Preview (push) Has been skipped
CI / Scraper / Lint (pull_request) Successful in 13s
CI / Scraper / Test (pull_request) Successful in 19s
CI / UI / Build (pull_request) Successful in 20s
CI / Scraper / Build (pull_request) Successful in 18s

This commit is contained in:
Admin
2026-03-06 18:41:30 +05:00
parent 60a9540ef7
commit 08d4718245
7 changed files with 218 additions and 15 deletions

View File

@@ -346,6 +346,82 @@ export async function mergeSessionProgress(sessionId: string, userId: string): P
log.info('pocketbase', 'mergeSessionProgress: done', { sessionId, userId, count: sessionRows.length });
}
// ─── User library (saved books) ───────────────────────────────────────────────
export interface UserLibraryEntry {
id?: string;
session_id: string;
user_id?: string;
slug: string;
saved_at: string;
}
function libraryFilter(sessionId: string, userId?: string): string {
if (userId) return `user_id="${userId}"`;
return `session_id="${sessionId}"`;
}
/** Returns all slugs the user has explicitly saved to their library. */
export async function getSavedSlugs(sessionId: string, userId?: string): Promise<Set<string>> {
const rows = await listAll<UserLibraryEntry>(
'user_library',
libraryFilter(sessionId, userId)
);
return new Set(rows.map((r) => r.slug));
}
/** Returns whether a specific slug is saved. */
export async function isBookSaved(
sessionId: string,
slug: string,
userId?: string
): Promise<boolean> {
const filter = userId
? `user_id="${userId}"&&slug="${slug}"`
: `session_id="${sessionId}"&&slug="${slug}"`;
const row = await listOne<UserLibraryEntry>('user_library', filter);
return row !== null;
}
/** Save a book to the user's library. No-op if already saved. */
export async function saveBook(
sessionId: string,
slug: string,
userId?: string
): Promise<void> {
const alreadySaved = await isBookSaved(sessionId, slug, userId);
if (alreadySaved) return;
const payload: Partial<UserLibraryEntry> = {
session_id: sessionId,
slug,
saved_at: new Date().toISOString()
};
if (userId) payload.user_id = userId;
const res = await pbPost('/api/collections/user_library/records', payload);
if (!res.ok) {
const body = await res.text().catch(() => '');
log.error('pocketbase', 'saveBook POST failed', { slug, status: res.status, body });
}
}
/** Remove a book from the user's library. */
export async function unsaveBook(
sessionId: string,
slug: string,
userId?: string
): Promise<void> {
const filter = userId
? `user_id="${userId}"&&slug="${slug}"`
: `session_id="${sessionId}"&&slug="${slug}"`;
const row = await listOne<UserLibraryEntry & { id: string }>('user_library', filter);
if (!row) return;
const token = await getToken();
await fetch(`${PB_URL}/api/collections/user_library/records/${row.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
});
}
// ─── Users ────────────────────────────────────────────────────────────────────
import { scryptSync, randomBytes, timingSafeEqual } from 'node:crypto';