Files
tiptoi-sync/client/src/pages/ScanPage.tsx
T
arnef 30170436fa Add scan and library UI pages
ScanPage (barcode/text search) and DownloadsPage (library + pen sync
status/progress) wired up via bottom-nav tabs in App.tsx.
2026-08-12 11:54:39 +02:00

184 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createSignal, For, Show } from 'solid-js'
import QrScanner from '../components/QrScanner'
import { searchBook, downloadBook, type GmeEntry } from '../api'
type State =
| { type: 'idle' }
| { type: 'searching' }
| { type: 'found'; title: string; gmeUrls: GmeEntry[] }
| { type: 'not_found' }
| { type: 'downloading' }
| { type: 'done'; filename: string }
| { type: 'error'; message: string }
export default function ScanPage() {
const [state, setState] = createSignal<State>({ type: 'idle' })
const [query, setQuery] = createSignal('')
const search = async (q: string) => {
if (!q.trim()) return
setState({ type: 'searching' })
try {
const data = await searchBook(q.trim())
if (data.error || !data.gmeUrls?.length) {
setState({ type: 'not_found' })
} else {
setState({ type: 'found', title: data.title!, gmeUrls: data.gmeUrls! })
}
} catch (e) {
setState({ type: 'error', message: e instanceof Error ? e.message : String(e) })
}
}
const download = async (gmeUrl: string, imageUrl?: string) => {
setState({ type: 'downloading' })
try {
const data = await downloadBook(gmeUrl, imageUrl)
if (data.filename) {
setState({ type: 'done', filename: data.filename })
} else {
setState({ type: 'error', message: data.message })
}
} catch (e) {
setState({ type: 'error', message: e instanceof Error ? e.message : String(e) })
}
}
const reset = () => {
setState({ type: 'idle' })
setQuery('')
}
const isIdle = () => ['idle', 'not_found', 'done'].includes(state().type)
return (
<div class="page">
{/* Suchfeld */}
<Show when={state().type === 'idle' || state().type === 'not_found'}>
<div>
<Show when={state().type === 'not_found'}>
<p class="hint" style="margin-bottom:0.6rem">
Kein Buch gefunden Titel oder Artikelnummer eingeben:
</p>
</Show>
<div class="search-row">
<input
class="input"
type="search"
placeholder="z. B. Tierkinder oder 55476"
value={query()}
onInput={e => setQuery(e.currentTarget.value)}
onKeyDown={e => e.key === 'Enter' && search(query())}
/>
<button class="btn" onClick={() => search(query())}>
<IconSearch />
</button>
</div>
</div>
</Show>
{/* Scanner */}
<Show when={isIdle() && 'BarcodeDetector' in window}>
<QrScanner onScan={search} />
</Show>
{/* Suche läuft */}
<Show when={state().type === 'searching'}>
<div class="status">
<span class="spinner" />
Suche läuft
</div>
</Show>
{/* Ergebnis */}
<Show when={state().type === 'found'}>
{(() => {
const s = state() as { type: 'found'; title: string; gmeUrls: GmeEntry[] }
return (
<div class="result-list">
<p class="result-card__title">{s.title}</p>
<For each={s.gmeUrls}>
{(entry) => (
<div class="card result-card">
<Show when={entry.imageUrl}>
<img src={entry.imageUrl!} class="result-card__image" alt={entry.label} />
</Show>
<button class="btn" onClick={() => download(entry.url, entry.imageUrl ?? undefined)}>
<IconDownload />
Herunterladen
</button>
</div>
)}
</For>
</div>
)
})()}
</Show>
{/* Download läuft */}
<Show when={state().type === 'downloading'}>
<div class="status">
<span class="spinner" />
Download läuft
</div>
</Show>
{/* Erfolg */}
<Show when={state().type === 'done'}>
{(() => {
const s = state() as { type: 'done'; filename: string }
return (
<div class="card">
<p class="status status--ok">
<IconCheck /> {s.filename} wurde gespeichert
</p>
</div>
)
})()}
</Show>
{/* Fehler */}
<Show when={state().type === 'error'}>
{(() => {
const s = state() as { type: 'error'; message: string }
return (
<div class="card">
<p class="status status--error">Fehler: {s.message}</p>
</div>
)
})()}
</Show>
{/* Zurück */}
<Show when={state().type !== 'idle'}>
<button class="btn btn--ghost" onClick={reset}>Neu scannen</button>
</Show>
</div>
)
}
function IconSearch() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
)
}
function IconDownload() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
</svg>
)
}
function IconCheck() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="18" height="18">
<polyline points="20 6 9 17 4 12"/>
</svg>
)
}