4 Commits

Author SHA1 Message Date
Roman Krček
a38edfbc07 Finalize transition to ScanWave and add dev dependecies 2025-06-25 15:11:40 +02:00
Roman Krček
6c2e80ecc1 Kill the QRcode scanner when exiting the view 2025-06-25 14:54:06 +02:00
Roman Krček
c28c338de4 Styling changes and rebrand to ScanWave 2025-06-25 14:53:51 +02:00
Roman Krček
906f0376d2 Event overview panel 2025-06-25 14:16:45 +02:00
16 changed files with 321 additions and 54 deletions

View File

@@ -46,7 +46,7 @@ jobs:
org.opencontainers.image.revision=${{ env.GITHUB_SHA }}
org.opencontainers.image.vendor=Orebolt.cz
org.opencontainers.image.ref.name=${{ env.GITHUB_REF }}
org.opencontainers.image.title=ESN Code Scanner App
org.opencontainers.image.title=ScanWave
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.24.0

12
docker-compose-prod.yml Normal file
View File

@@ -0,0 +1,12 @@
services:
app:
image: ${DOCKER_REGISTRY}/${DOCKER_USER}/${DOCKER_IMAGE}:latest
restart: unless-stopped
env_file: .env
labels:
- "traefik.enable=true"
- "traefik.http.routers.scan-wave.rule=Host(`scanwave.orebolt.cz`)"
- "traefik.http.routers.scan-wave.tls.certresolver=leresolver"
- "traefik.http.routers.scan-wave.entrypoints=websecure"
- "traefik.http.services.scan-wave.loadbalancer.server.port=3000"
- "traefik.http.routers.scan-wave.middlewares=hsts"

View File

@@ -1,7 +1,7 @@
---
services:
app:
image: ${DOCKER_REGISTRY}/${DOCKER_USER}/esn-code-scanner-app:latest
image: ${DOCKER_REGISTRY}/${DOCKER_USER}/${DOCKER_IAMGE}:latest
restart: unless-stopped
ports:
- "3000:3000"

93
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "esn-code-scanner",
"name": "scan-wave",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "esn-code-scanner",
"name": "scan-wave",
"version": "0.0.1",
"dependencies": {
"@supabase/ssr": "^0.6.1",
@@ -18,7 +18,7 @@
"simple-icons": "^15.3.0"
},
"devDependencies": {
"@sveltejs/kit": "^2.16.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
@@ -30,7 +30,7 @@
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.6"
"vite-plugin-devtools-json": "^0.2.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -58,6 +58,7 @@
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -74,6 +75,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -90,6 +92,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -106,6 +109,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -122,6 +126,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -138,6 +143,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -154,6 +160,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -170,6 +177,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -186,6 +194,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -202,6 +211,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -218,6 +228,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -234,6 +245,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -250,6 +262,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -266,6 +279,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -282,6 +296,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -298,6 +313,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -314,6 +330,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -330,6 +347,7 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -346,6 +364,7 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -362,6 +381,7 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -378,6 +398,7 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -394,6 +415,7 @@
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -410,6 +432,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -426,6 +449,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -442,6 +466,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -994,15 +1019,15 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.21.0.tgz",
"integrity": "sha512-kvu4h9qXduiPk1Q1oqFKDLFGu/7mslEYbVaqpbBcBxjlRJnvNCFwEvEwKt0Mx9TtSi8J77xRelvJobrGlst4nQ==",
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.0.tgz",
"integrity": "sha512-DJm0UxVgzXq+1MUfiJK4Ridk7oIQsIets6JwHiEl97sI6nXScfXe+BeqNhzB7jQIVBb3BM51U4hNk8qQxRXBAA==",
"license": "MIT",
"dependencies": {
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/cookie": "^0.6.0",
"acorn": "^8.14.1",
"cookie": "^0.7.0",
"cookie": "^0.6.0",
"devalue": "^5.1.0",
"esm-env": "^1.2.2",
"kleur": "^4.1.5",
@@ -1010,7 +1035,8 @@
"mrmime": "^2.0.0",
"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
"sirv": "^3.0.0"
"sirv": "^3.0.0",
"vitefu": "^1.0.6"
},
"bin": {
"svelte-kit": "svelte-kit.js"
@@ -1019,9 +1045,9 @@
"node": ">=18.13"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.3 || ^6.0.0"
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
@@ -1604,9 +1630,9 @@
"license": "MIT"
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -1770,6 +1796,7 @@
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -2570,6 +2597,7 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -2730,6 +2758,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -3236,6 +3265,13 @@
"typescript": ">=5.0.0"
}
},
"node_modules/svelte-kit": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/svelte-kit/-/svelte-kit-1.2.0.tgz",
"integrity": "sha512-RRaOHBhpDv4g2v9tcq8iNw055Pt0MlLps6JVA7/40f4KAbtztXSI4T6MZYbHRirO708urfAAMx6Qow+tQfCHug==",
"hasInstallScript": true,
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",
@@ -3276,6 +3312,7 @@
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"license": "MIT",
"peer": true,
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
@@ -3349,11 +3386,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -3423,6 +3475,19 @@
}
}
},
"node_modules/vite-plugin-devtools-json": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-devtools-json/-/vite-plugin-devtools-json-0.2.0.tgz",
"integrity": "sha512-K7PoaWOEJECZ1n3VbhJXsUAX2PsO0xY7KFMM/Leh7tUev0M5zi+lz+vnVVdCK17IOK9Jp9rdzHXc08cnQirGbg==",
"dev": true,
"license": "MIT",
"dependencies": {
"uuid": "^11.1.0"
},
"peerDependencies": {
"vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
}
},
"node_modules/vitefu": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",

View File

@@ -1,5 +1,5 @@
{
"name": "esn-code-scanner",
"name": "scan-wave",
"private": true,
"version": "0.0.1",
"type": "module",
@@ -14,7 +14,7 @@
"lint": "prettier --check ."
},
"devDependencies": {
"@sveltejs/kit": "^2.16.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
@@ -26,7 +26,7 @@
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.6"
"vite-plugin-devtools-json": "^0.2.0"
},
"dependencies": {
"@supabase/ssr": "^0.6.1",

View File

@@ -1,20 +1,24 @@
<script>
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
import "../app.css";
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
import '../app.css';
let { data, children } = $props()
let { session, supabase } = $derived(data)
let { data, children } = $props();
let { session, supabase } = $derived(data);
onMount(() => {
const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
if (newSession?.expires_at !== session?.expires_at) {
invalidate('supabase:auth')
}
})
onMount(() => {
const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
if (newSession?.expires_at !== session?.expires_at) {
invalidate('supabase:auth');
}
});
return () => data.subscription.unsubscribe()
})
return () => data.subscription.unsubscribe();
});
</script>
{@render children()}
<svelte:head>
<title>ScanWave</title>
</svelte:head>
{@render children()}

View File

@@ -3,9 +3,11 @@
<div class="mb-8">
<img class="w-32 h-auto" src="/qr-code.png" alt="">
</div>
<h1 class="text-3xl font-bold text-center mb-2">ESN Scanner App</h1>
<h1 class="text-3xl font-bold text-center mb-2">ScanWave</h1>
<h2 class="text-lg text-gray-600 text-center mb-8">Make entrance to your events a breeze.</h2>
<div class="flex space-x-4 w-full justify-center">
<a href="/private/home" class="w-64 py-2 bg-blue-600 text-white rounded-lg text-center hover:bg-blue-700 transition">Get started</a>
<a href="/private/home" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-full shadow-none border border-gray-300 w-64 text-center transition">
Get started
</a>
</div>
</div>

View File

@@ -2,13 +2,18 @@
// Add any navbar logic here if needed
</script>
<nav class="bg-white border-b border-gray-300 text-gray-900 p-4 flex items-center justify-between">
<div class="font-bold text-lg">ESN Scanner</div>
<ul class="flex space-x-4">
<li><a href="/private/home" class="hover:underline">Home</a></li>
<li><a href="/private/scanner" class="hover:underline">Scanner</a></li>
<li><a href="/private/events" class="hover:underline">Events</a></li>
</ul>
<nav class="bg-gray-50 border-b border-gray-300 text-gray-900 p-2">
<div class="container max-w-2xl mx-auto p-2">
<div class="flex items-center justify-between">
<div class="font-bold text-lg">ScanWave</div>
<ul class="flex space-x-4">
<li><a href="/private/home" class="hover:underline">Home</a></li>
<li><a href="/private/scanner" class="hover:underline">Scanner</a></li>
<li><a href="/private/events" class="hover:underline">Events</a></li>
</ul>
</div>
</div>
</nav>
<div class="container max-w-2xl mx-auto p-2 bg-white">

View File

@@ -0,0 +1,7 @@
export async function load({ locals }) {
const { data: events, error } = await locals.supabase
.from('events')
.select('*')
.order('date', { ascending: false });
return { events };
}

View File

@@ -0,0 +1,25 @@
<script lang="ts">
export let data;
</script>
<h1 class="text-2xl font-bold mb-4 mt-2 text-center">All Events</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto">
{#each data.events as event}
<a
href={`/private/events/event?id=${event.id}`}
class="block border border-gray-300 rounded bg-white p-4 shadow-none transition cursor-pointer hover:border-blue-500 group"
>
<div class="flex flex-col gap-1">
<span class="font-semibold text-lg text-black-700 group-hover:underline">{event.name}</span>
<span class="text-gray-500 text-sm">{event.date}</span>
</div>
</a>
{/each}
</div>
<a
href="/private/creator"
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-full shadow-none border border-gray-300"
>
New Event
</a>

View File

@@ -0,0 +1,13 @@
export async function load({ locals, url }) {
const event_id = url.searchParams.get('id');
const { data: event_data, error: eventError } = await locals.supabase
.from('events')
.select('*')
.eq('id', event_id)
.single()
const { data: participants, error: participantsError } = await locals.supabase
.from('participants')
.select('*, scanned_by:profiles (id, display_name)')
.eq('event', event_id)
return {event_data, participants};
}

View File

@@ -0,0 +1,95 @@
<script lang="ts">
let { data } = $props();
const scannedCount = data.participants.filter((p) => p.scanned).length;
const notScannedCount = data.participants.length - scannedCount;
</script>
<h1 class="mt-2 mb-4 text-center text-2xl font-bold">Event Overview</h1>
<div class="mb-2 rounded border border-gray-300 bg-white p-4">
<div class="flex flex-col gap-1">
<span class="text-black-700 text-lg font-semibold">{data.event_data.name}</span>
<span class="text-black-500 text-sm">{data.event_data.date}</span>
</div>
</div>
<div class="mb-2 flex items-center rounded border border-gray-300 bg-white p-4">
<div class="flex flex-1 items-center justify-center gap-2">
<svg
class="inline h-4 w-4 text-green-600"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span class="text-sm text-gray-700">Scanned ({scannedCount})</span>
</div>
<div class="mx-4 h-8 w-px bg-gray-300"></div>
<div class="flex flex-1 items-center justify-center gap-2">
<svg
class="inline h-4 w-4 text-red-600"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" />
<line x1="8" y1="8" x2="16" y2="16" stroke="currentColor" stroke-width="2" />
<line x1="16" y1="8" x2="8" y2="16" stroke="currentColor" stroke-width="2" />
</svg>
<span class="text-sm text-gray-700">Not scanned ({notScannedCount})</span>
</div>
</div>
<div class="rounded border border-gray-300 bg-white p-4">
<h2 class="mb-2 rounded text-xl font-bold">Participants ({data.participants.length})</h2>
<ul class="space-y-1">
{#each data.participants as p}
<li class="flex items-center gap-2 border-b pb-1 last:border-b-0">
{#if p.scanned}
<svg
title="Scanned"
class="mr-2 inline h-4 w-4 text-green-600"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
{:else}
<svg
title="Not scanned"
class="mr-2 inline h-4 w-4 text-red-600"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" />
<line x1="8" y1="8" x2="16" y2="16" stroke="currentColor" stroke-width="2" />
<line x1="16" y1="8" x2="8" y2="16" stroke="currentColor" stroke-width="2" />
</svg>
{/if}
<span class="font-semibold">{p.name} {p.surname}</span>
<span class="flex-1"></span>
{#if p.scanned_by}
<div class="flex flex-row items-end ml-2">
<span class="mr-1 text-xs text-gray-500">
{new Date(p.scanned_at).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: false
})}
</span>
<span class="text-xs text-gray-500">by {p.scanned_by.display_name}</span>
</div>
{/if}
</li>
{/each}
</ul>
</div>

View File

@@ -7,13 +7,45 @@
};
</script>
<div class="user-profile">
<h2 class="mb-2 text-2xl font-bold">Currently logged in</h2>
<p><strong>Username:</strong> {data.user?.user_metadata.display_name}</p>
<p><strong>Email:</strong> {data.user?.email}</p>
<p><strong>Section:</strong> {data.user_profile?.section.name}</p>
<p><strong>Position:</strong> {data.user_profile?.section_position}</p>
<h1 class="mt-2 mb-4 text-center text-2xl font-bold">User Profile</h1>
<div class="mb-4 rounded border border-gray-300 bg-white p-6">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3 mb-4">
<div class="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center text-xl font-bold text-gray-600">
{data.user?.user_metadata.display_name?.[0] ?? "U"}
</div>
<div>
<span class="text-lg font-semibold text-gray-800">{data.user?.user_metadata.display_name}</span>
<div class="text-sm text-gray-500">{data.user?.email}</div>
</div>
</div>
<div class="flex flex-col gap-1">
<div>
<span class="font-medium text-gray-700">Section:</span>
<span class="text-gray-900">{data.user_profile?.section.name ?? "N/A"}</span>
</div>
<div>
<span class="font-medium text-gray-700">Position:</span>
<span class="text-gray-900">{data.user_profile?.section_position ?? "N/A"}</span>
</div>
</div>
<h2 class="text-lg mb-2 mt-4">User guide</h2>
<p class="text-gray-700 text-sm leading-relaxed">
To scan a QR code, head over to Scanner in the top right corner. Click on Start scanning and allow camera permissions.
If you close and open your browser and your camera is stuck, simply refresh the page or click Stop scanning and then Start scanning again.
When you scan a QR code, a request is sent to the server to get the user's personal information and to mark their tickets as scanned.
</p>
<h2 class="text-lg mb-2 mt-4">Administrator guide</h2>
<p class="text-gray-700 text-sm leading-relaxed">
You can view events
</p>
</div>
</div>
<button class="mt-4 rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600">
<a href="/auth/signout">Sign out</a>
</button>
<a
href="/auth/signout"
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-red-500 hover:bg-red-600 text-white font-semibold py-3 px-8 rounded-full shadow-none border border-gray-300 transition"
>
Sign out
</a>

View File

@@ -15,7 +15,7 @@
scan_state = ScanState.scanning;
data.supabase
.from('qrcodes')
.from('participants')
.select(`*, event ( id, name ), scanned_by ( id, display_name )`)
.eq('id', scanned_id)
.then( response => {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import {
Html5QrcodeScanner,
type Html5QrcodeResult,
@@ -37,6 +37,12 @@
);
scanner.render(onScanSuccess, onScanFailure);
});
onDestroy(() => {
if (scanner) {
scanner.clear().catch(() => {});
}
});
</script>
<div id="qr-scanner" class="w-full h-full max-w-none overflow-hidden rounded-sm"></div>

View File

@@ -1,7 +1,8 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import devtoolsJson from 'vite-plugin-devtools-json';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
plugins: [tailwindcss(), sveltekit(), devtoolsJson()]
});