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:
@@ -8,6 +8,7 @@
|
||||
"@nuxt/fonts": "^0.13.0",
|
||||
"@nuxt/ui": "^4.4.0",
|
||||
"@supabase/supabase-js": "^2.95.3",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"nuxt": "^4.3.1",
|
||||
"vue": "^3.5.28",
|
||||
"vue-router": "^4.6.4",
|
||||
@@ -1076,6 +1077,8 @@
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"html5-qrcode": ["html5-qrcode@2.3.8", "", {}, "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ=="],
|
||||
|
||||
"http-assert": ["http-assert@1.5.0", "", { "dependencies": { "deep-equal": "~1.0.1", "http-errors": "~1.8.0" } }, "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
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>
|
||||
@@ -13,6 +13,7 @@
|
||||
"@nuxt/fonts": "^0.13.0",
|
||||
"@nuxt/ui": "^4.4.0",
|
||||
"@supabase/supabase-js": "^2.95.3",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"nuxt": "^4.3.1",
|
||||
"vue": "^3.5.28",
|
||||
"vue-router": "^4.6.4"
|
||||
|
||||
Reference in New Issue
Block a user