feat: add local development setup with Docker Compose
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
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
This commit is contained in:
161
app/components/scan/BarcodeScanner.vue
Normal file
161
app/components/scan/BarcodeScanner.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user