Some checks failed
Pull Request Checks / Validate PR (pull_request) Has been cancelled
Deploy to Coolify / Code Quality (pull_request) Has been cancelled
Deploy to Coolify / Run Tests (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Development (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Production (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Test (pull_request) Has been cancelled
Complete local dev environment for testing: **Docker Compose Stack:** - PostgreSQL 15 (Supabase) - GoTrue (Auth service) - PostgREST (Auto-generated API) - Kong (API Gateway) - Realtime (WebSocket subscriptions) - Storage API (S3-compatible) - Supabase Studio (Admin UI on :54323) **Configuration:** - Kong routing config for all Supabase services - Environment variables with example JWT/API keys - Auto-apply migrations on first startup - Persistent volumes for data **Documentation:** - DEV_SETUP.md with step-by-step guide - Troubleshooting section - Common tasks (reset DB, view logs, etc.) - Pre-seeded data reference **Bonus:** - BarcodeScanner.vue component (Week 3 preview) - html5-qrcode library installed Ready to run: `docker-compose up -d && cd app && bun run dev` Access: - App: http://localhost:3000 - Supabase API: http://localhost:54321 - Supabase Studio: http://localhost:54323 - PostgreSQL: localhost:5432
162 lines
4.3 KiB
Vue
162 lines
4.3 KiB
Vue
<template>
|
|
<div class="relative">
|
|
<!-- Scanner Container -->
|
|
<div
|
|
:id="scannerId"
|
|
ref="scannerRef"
|
|
class="w-full rounded-lg overflow-hidden bg-black"
|
|
:class="{ 'aspect-video': !isScanning, 'min-h-[300px]': isScanning }"
|
|
/>
|
|
|
|
<!-- Overlay when not scanning -->
|
|
<div
|
|
v-if="!isScanning && !error"
|
|
class="absolute inset-0 flex items-center justify-center bg-gray-900 rounded-lg"
|
|
>
|
|
<div class="text-center">
|
|
<UIcon name="i-heroicons-camera" class="w-16 h-16 text-gray-400 mb-4" />
|
|
<UButton
|
|
color="primary"
|
|
size="lg"
|
|
icon="i-heroicons-qr-code"
|
|
@click="startScanning"
|
|
>
|
|
Start Camera
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div
|
|
v-if="error"
|
|
class="absolute inset-0 flex items-center justify-center bg-gray-900 rounded-lg"
|
|
>
|
|
<div class="text-center px-4">
|
|
<UIcon name="i-heroicons-exclamation-triangle" class="w-12 h-12 text-red-400 mb-4" />
|
|
<p class="text-white mb-4">{{ error }}</p>
|
|
<div class="flex gap-2 justify-center">
|
|
<UButton @click="startScanning" color="primary">Try Again</UButton>
|
|
<UButton @click="$emit('manual-entry')" color="gray">Enter Manually</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manual Barcode Entry -->
|
|
<div class="mt-4">
|
|
<UFormGroup label="Or enter barcode manually">
|
|
<div class="flex gap-2">
|
|
<UInput
|
|
v-model="manualBarcode"
|
|
placeholder="e.g. 8000500310427"
|
|
size="lg"
|
|
class="flex-1"
|
|
@keyup.enter="submitManualBarcode"
|
|
/>
|
|
<UButton
|
|
color="primary"
|
|
size="lg"
|
|
:disabled="!manualBarcode.trim()"
|
|
@click="submitManualBarcode"
|
|
>
|
|
Lookup
|
|
</UButton>
|
|
</div>
|
|
</UFormGroup>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode'
|
|
|
|
const emit = defineEmits<{
|
|
'barcode-detected': [barcode: string]
|
|
'manual-entry': []
|
|
}>()
|
|
|
|
const scannerId = 'barcode-scanner'
|
|
const scannerRef = ref<HTMLElement | null>(null)
|
|
const isScanning = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const manualBarcode = ref('')
|
|
|
|
let html5QrCode: Html5Qrcode | null = null
|
|
|
|
const startScanning = async () => {
|
|
error.value = null
|
|
|
|
try {
|
|
if (!html5QrCode) {
|
|
html5QrCode = new Html5Qrcode(scannerId, {
|
|
formatsToSupport: [
|
|
Html5QrcodeSupportedFormats.EAN_13,
|
|
Html5QrcodeSupportedFormats.EAN_8,
|
|
Html5QrcodeSupportedFormats.UPC_A,
|
|
Html5QrcodeSupportedFormats.UPC_E,
|
|
Html5QrcodeSupportedFormats.CODE_128,
|
|
Html5QrcodeSupportedFormats.CODE_39,
|
|
Html5QrcodeSupportedFormats.QR_CODE
|
|
],
|
|
verbose: false
|
|
})
|
|
}
|
|
|
|
await html5QrCode.start(
|
|
{ facingMode: 'environment' },
|
|
{
|
|
fps: 10,
|
|
qrbox: { width: 250, height: 150 },
|
|
aspectRatio: 1.777
|
|
},
|
|
onScanSuccess,
|
|
onScanFailure
|
|
)
|
|
|
|
isScanning.value = true
|
|
} catch (err: any) {
|
|
console.error('Scanner error:', err)
|
|
|
|
if (err.toString().includes('NotAllowedError')) {
|
|
error.value = 'Camera permission denied. Please allow camera access and try again.'
|
|
} else if (err.toString().includes('NotFoundError')) {
|
|
error.value = 'No camera found. Please use a device with a camera or enter the barcode manually.'
|
|
} else {
|
|
error.value = 'Could not start camera. Try entering the barcode manually.'
|
|
}
|
|
}
|
|
}
|
|
|
|
const stopScanning = async () => {
|
|
if (html5QrCode && isScanning.value) {
|
|
try {
|
|
await html5QrCode.stop()
|
|
} catch (err) {
|
|
console.error('Error stopping scanner:', err)
|
|
}
|
|
isScanning.value = false
|
|
}
|
|
}
|
|
|
|
const onScanSuccess = (decodedText: string) => {
|
|
// Stop scanning after successful read
|
|
stopScanning()
|
|
emit('barcode-detected', decodedText)
|
|
}
|
|
|
|
const onScanFailure = (_errorMessage: string) => {
|
|
// Ignore - this fires continuously when no barcode is detected
|
|
}
|
|
|
|
const submitManualBarcode = () => {
|
|
if (manualBarcode.value.trim()) {
|
|
emit('barcode-detected', manualBarcode.value.trim())
|
|
manualBarcode.value = ''
|
|
}
|
|
}
|
|
|
|
// Cleanup on unmount
|
|
onUnmounted(() => {
|
|
stopScanning()
|
|
})
|
|
</script>
|