Enhance routing and layout in SvelteKit project by adding new routes for accounts and transactions, updating existing routes, and improving layout parameters. Refactor item handling and integrate barcode scanning functionality for better user experience.
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Search, X } from "@lucide/svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onResult,
|
||||
title = "Scan barcode",
|
||||
}: {
|
||||
open?: boolean;
|
||||
onResult?: (value: string) => void;
|
||||
title?: string;
|
||||
} = $props();
|
||||
|
||||
let scanError = $state<string | null>(null);
|
||||
let searchInput = $state("");
|
||||
let videoEl = $state<HTMLVideoElement | undefined>(undefined);
|
||||
let stream = $state<MediaStream | null>(null);
|
||||
let scanRAF = $state(0);
|
||||
|
||||
function submitSearch() {
|
||||
const value = searchInput.trim();
|
||||
if (!value) return;
|
||||
stopScan();
|
||||
open = false;
|
||||
searchInput = "";
|
||||
onResult?.(value);
|
||||
}
|
||||
|
||||
async function ensureBarcodeDetector(): Promise<boolean> {
|
||||
if (typeof globalThis === "undefined" || "BarcodeDetector" in globalThis) return true;
|
||||
try {
|
||||
const mod = await import("barcode-detector-api-polyfill");
|
||||
(globalThis as unknown as { BarcodeDetector: typeof BarcodeDetector }).BarcodeDetector =
|
||||
mod.BarcodeDetector;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startScan() {
|
||||
scanError = null;
|
||||
const hasDetector = await ensureBarcodeDetector();
|
||||
if (!hasDetector) {
|
||||
scanError = "Failed to load barcode scanner. Try again or use Chrome/Edge.";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 } },
|
||||
});
|
||||
} catch (e) {
|
||||
scanError = e instanceof Error ? e.message : "Could not access camera.";
|
||||
}
|
||||
}
|
||||
|
||||
function stopScan() {
|
||||
if (scanRAF) {
|
||||
cancelAnimationFrame(scanRAF);
|
||||
scanRAF = 0;
|
||||
}
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((t) => t.stop());
|
||||
stream = null;
|
||||
}
|
||||
if (videoEl?.srcObject) {
|
||||
videoEl.srcObject = null;
|
||||
}
|
||||
scanError = null;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
stopScan();
|
||||
searchInput = "";
|
||||
return;
|
||||
}
|
||||
startScan();
|
||||
return () => stopScan();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!open || !stream || !videoEl) return;
|
||||
videoEl.srcObject = stream;
|
||||
videoEl.play().catch(() => {});
|
||||
return () => {
|
||||
if (videoEl?.srcObject) videoEl.srcObject = null;
|
||||
};
|
||||
});
|
||||
|
||||
const SCAN_INTERVAL_MS = 300;
|
||||
|
||||
function scanLoop() {
|
||||
if (!videoEl || !("BarcodeDetector" in globalThis)) return;
|
||||
const detector = new (globalThis as unknown as { BarcodeDetector: typeof BarcodeDetector }).BarcodeDetector();
|
||||
let lastScan = 0;
|
||||
async function detect() {
|
||||
if (!open || !videoEl) return;
|
||||
if (videoEl.readyState < 2) {
|
||||
scanRAF = requestAnimationFrame(detect);
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now - lastScan >= SCAN_INTERVAL_MS) {
|
||||
lastScan = now;
|
||||
try {
|
||||
const barcodes = await detector.detect(videoEl);
|
||||
if (barcodes.length > 0) {
|
||||
const value = barcodes[0].rawValue?.trim() ?? "";
|
||||
stopScan();
|
||||
open = false;
|
||||
onResult?.(value);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
scanRAF = requestAnimationFrame(detect);
|
||||
}
|
||||
detect();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!open || !stream || !videoEl) return;
|
||||
scanLoop();
|
||||
return () => {
|
||||
if (scanRAF) cancelAnimationFrame(scanRAF);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content
|
||||
class="flex max-h-[100dvh] w-[100vw] max-w-none flex-col gap-4 rounded-none border-0 p-4 sm:max-h-[90dvh] sm:w-auto sm:max-w-md sm:rounded-lg sm:border"
|
||||
onclose={() => stopScan()}
|
||||
>
|
||||
<Dialog.Header class="shrink-0">
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>Scan a barcode or type a code to look up an item.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4">
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Type barcode or SKU to search…"
|
||||
bind:value={searchInput}
|
||||
onkeydown={(e) => e.key === "Enter" && submitSearch()}
|
||||
class="min-h-11 flex-1"
|
||||
/>
|
||||
<Button type="button" class="min-h-11 shrink-0" onclick={submitSearch} disabled={!searchInput.trim()}>
|
||||
<Search class="mr-2 size-4" />
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-muted-foreground shrink-0 text-center text-xs">or use camera below</p>
|
||||
{#if scanError}
|
||||
<p class="text-destructive text-sm">{scanError}</p>
|
||||
{:else if stream}
|
||||
<video
|
||||
bind:this={videoEl}
|
||||
class="border-input max-h-[50vh] w-full flex-1 rounded-lg border object-cover sm:max-h-none"
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-sm">Starting camera…</p>
|
||||
{/if}
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="min-h-12 flex-1"
|
||||
onclick={() => { stopScan(); open = false; }}
|
||||
>
|
||||
<X class="mr-2 size-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
Reference in New Issue
Block a user