2026-08-15 10:32:13 -04:00
|
|
|
import { sql } from '../../../lib/sql.js';
|
feat(scanner): rebuild as mobile-first three-phase flow
Replace the desktop-first, everything-at-once scanner layout with a
phased mobile-optimized experience: Setup → Scanning → Review.
Phase 1 (Setup): destination picker, game filter, deck mode toggle,
scan history (last 5 sessions).
Phase 2 (Scanning): full-screen camera with auto-start, haptic + sound
feedback on card detection, torch/flash toggle, count pill, bottom-sheet
disambiguation (replaces full-screen modal).
Phase 3 (Review): card list with inline condition/foil/qty edits,
batch confirm, 30-second undo, deck progress indicator.
New features:
- Deck mode (progress toward 40/60/99 card target)
- Scan history (persisted to localStorage)
- Sound feedback (Web Audio oscillator, configurable)
- Offline queue (localStorage persistence + auto-retry on reconnect)
- Camera flash/torch toggle
- Batch ownership API (replaces N+1 per-card fetches)
- Visibility pause (detection loop stops when tab is backgrounded)
Convoy: scanner-rebuild
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 09:42:58 -04:00
|
|
|
import { getUserFromRequest } from '../../../lib/permission-middleware';
|
|
|
|
|
|
|
|
|
|
export default async function handler(req, res) {
|
|
|
|
|
if (req.method !== 'POST') {
|
|
|
|
|
return res.status(405).json({ error: 'Method not allowed' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const user = await getUserFromRequest(req);
|
|
|
|
|
if (!user) {
|
|
|
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { cardIds } = req.body || {};
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(cardIds) || cardIds.length === 0) {
|
|
|
|
|
return res.status(400).json({ error: 'cardIds must be a non-empty array' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cardIds.length > 100) {
|
|
|
|
|
return res.status(400).json({ error: 'cardIds cannot exceed 100 items' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const valid = cardIds.every(
|
|
|
|
|
(id) => Number.isInteger(id) && id > 0
|
|
|
|
|
);
|
|
|
|
|
if (!valid) {
|
|
|
|
|
return res.status(400).json({ error: 'All cardIds must be positive integers' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await sql`
|
|
|
|
|
SELECT card_id, SUM(quantity)::int AS total_quantity
|
|
|
|
|
FROM user_cards
|
|
|
|
|
WHERE user_id = ${user.userId}
|
|
|
|
|
AND card_id = ANY(${cardIds})
|
|
|
|
|
GROUP BY card_id
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const ownership = {};
|
|
|
|
|
for (const row of result.rows) {
|
|
|
|
|
ownership[row.card_id] = row.total_quantity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res.status(200).json({ ownership });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[POST /api/cards/batch-ownership]', error);
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
}
|