feat(discover): Tinder-style book discovery + fix duplicate books
Some checks failed
CI / UI (push) Failing after 22s
CI / Backend (push) Successful in 53s
Release / Test backend (push) Successful in 24s
Release / Check ui (push) Failing after 24s
Release / Docker / ui (push) Has been skipped
CI / Backend (pull_request) Successful in 25s
CI / UI (pull_request) Failing after 22s
Release / Docker / caddy (push) Successful in 56s
Release / Docker / backend (push) Successful in 1m38s
Release / Docker / runner (push) Successful in 3m14s
Release / Gitea Release (push) Has been skipped
Some checks failed
CI / UI (push) Failing after 22s
CI / Backend (push) Successful in 53s
Release / Test backend (push) Successful in 24s
Release / Check ui (push) Failing after 24s
Release / Docker / ui (push) Has been skipped
CI / Backend (pull_request) Successful in 25s
CI / UI (pull_request) Failing after 22s
Release / Docker / caddy (push) Successful in 56s
Release / Docker / backend (push) Successful in 1m38s
Release / Docker / runner (push) Successful in 3m14s
Release / Gitea Release (push) Has been skipped
- New /discover page with swipe UI: left=skip, right=like, up=read now, down=nope - Onboarding modal to collect genre/status preferences (persisted in localStorage) - 3-card stack with pointer-event drag, CSS fly-out animation, 5 action buttons - Tap card for preview modal; empty state with deck reset - Like/read-now auto-saves book to user library - POST /api/discover/vote + DELETE for deck reset - Discovery vote persistence via PocketBase discovery_votes collection - Fix duplicate books: dedup by slug in getBooksBySlugs - Fix WriteMetadata TOCTOU race: conflict-retry on concurrent insert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
32
ui/src/routes/api/discover/vote/+server.ts
Normal file
32
ui/src/routes/api/discover/vote/+server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { upsertDiscoveryVote, clearDiscoveryVotes, saveBook } from '$lib/server/pocketbase';
|
||||
|
||||
const VALID_ACTIONS = ['like', 'skip', 'nope', 'read_now'] as const;
|
||||
type Action = (typeof VALID_ACTIONS)[number];
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!body || typeof body.slug !== 'string' || !VALID_ACTIONS.includes(body.action)) {
|
||||
error(400, 'Expected { slug, action }');
|
||||
}
|
||||
const action = body.action as Action;
|
||||
try {
|
||||
await upsertDiscoveryVote(locals.sessionId, body.slug, action, locals.user?.id);
|
||||
if (action === 'like' || action === 'read_now') {
|
||||
await saveBook(locals.sessionId, body.slug, locals.user?.id);
|
||||
}
|
||||
} catch (e) {
|
||||
error(500, 'Failed to record vote');
|
||||
}
|
||||
return json({ ok: true });
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ locals }) => {
|
||||
try {
|
||||
await clearDiscoveryVotes(locals.sessionId, locals.user?.id);
|
||||
} catch {
|
||||
error(500, 'Failed to clear votes');
|
||||
}
|
||||
return json({ ok: true });
|
||||
};
|
||||
Reference in New Issue
Block a user