2026-06-02 17:15:27 -04:00
|
|
|
import { VOCAB } from './collection-vocabulary.js';
|
|
|
|
|
|
|
|
|
|
function authHeaders(json = true) {
|
|
|
|
|
const headers = {
|
|
|
|
|
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
|
|
|
|
|
};
|
|
|
|
|
if (json) {
|
|
|
|
|
headers['Content-Type'] = 'application/json';
|
|
|
|
|
}
|
|
|
|
|
return headers;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildScannerCardPayload(cardData) {
|
|
|
|
|
const payload = {
|
|
|
|
|
cardId: cardData.databaseId,
|
|
|
|
|
quantity: cardData.quantity || 1,
|
|
|
|
|
condition: cardData.condition || 'NM',
|
|
|
|
|
is_foil: Boolean(cardData.isFoil),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (cardData.scanImageUrl) {
|
|
|
|
|
payload.scan_image_url = cardData.scanImageUrl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchScannerCollections() {
|
|
|
|
|
const response = await fetch('/api/collections', { headers: authHeaders(false) });
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Failed to load lists');
|
|
|
|
|
}
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
return data.filter((collection) => !collection.is_system_collection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchScannerDecks() {
|
|
|
|
|
const response = await fetch('/api/decks', { headers: authHeaders(false) });
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Failed to load decks');
|
|
|
|
|
}
|
|
|
|
|
return response.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function addScannedCardToOwned(cardData) {
|
|
|
|
|
const response = await fetch('/api/user-cards', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: authHeaders(),
|
|
|
|
|
body: JSON.stringify(buildScannerCardPayload(cardData)),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Failed to add to ${VOCAB.MY_COLLECTION}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function addScannedCardToCollection(cardData, collectionId) {
|
|
|
|
|
const response = await fetch(`/api/collections/${collectionId}/cards`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: authHeaders(),
|
|
|
|
|
body: JSON.stringify(buildScannerCardPayload(cardData)),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Failed to add to ${VOCAB.LIST}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function addScannedCardToDeck(cardData, deckId) {
|
|
|
|
|
const response = await fetch(`/api/decks/${deckId}/cards`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: authHeaders(),
|
|
|
|
|
body: JSON.stringify(buildScannerCardPayload(cardData)),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Failed to add to deck');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createScannerCollection(name) {
|
|
|
|
|
const response = await fetch('/api/collections', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: authHeaders(),
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
name,
|
|
|
|
|
description: 'Created from card scanner',
|
|
|
|
|
is_public: false,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Failed to create list');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response.json();
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
export async function fetchBatchOwnership(cardIds) {
|
|
|
|
|
const response = await fetch('/api/cards/batch-ownership', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: authHeaders(),
|
|
|
|
|
body: JSON.stringify({ cardIds }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Failed to fetch ownership data');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
return data.ownership;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 17:15:27 -04:00
|
|
|
export async function routeScannedCardToDestination(cardData, destination) {
|
|
|
|
|
if (!destination || !cardData.databaseId) return false;
|
|
|
|
|
|
|
|
|
|
if (destination.type === 'owned') {
|
|
|
|
|
await addScannedCardToOwned(cardData);
|
|
|
|
|
} else if (destination.type === 'collection') {
|
|
|
|
|
await addScannedCardToCollection(cardData, destination.id);
|
|
|
|
|
} else if (destination.type === 'deck') {
|
|
|
|
|
await addScannedCardToDeck(cardData, destination.id);
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|