supabase #1

Merged
erman merged 9 commits from supabase into main 2025-06-21 22:10:09 +02:00
26 changed files with 668 additions and 83 deletions

170
package-lock.json generated
View File

@@ -8,6 +8,8 @@
"name": "esn-code-scanner",
"version": "0.0.1",
"dependencies": {
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0",
"@sveltejs/adapter-node": "^5.2.12"
},
"devDependencies": {
@@ -867,6 +869,101 @@
"win32"
]
},
"node_modules/@supabase/auth-js": {
"version": "2.70.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz",
"integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz",
"integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/node-fetch": {
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "1.19.4",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
"integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.11.10",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz",
"integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.13",
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"ws": "^8.18.2"
}
},
"node_modules/@supabase/ssr": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.6.1.tgz",
"integrity": "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.43.4"
}
},
"node_modules/@supabase/ssr/node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
"integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.50.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz",
"integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.70.0",
"@supabase/functions-js": "2.4.4",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.19.4",
"@supabase/realtime-js": "2.11.10",
"@supabase/storage-js": "2.7.1"
}
},
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
@@ -1265,12 +1362,36 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT"
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
@@ -2346,6 +2467,12 @@
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -2360,6 +2487,12 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -2459,6 +2592,43 @@
}
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",

View File

@@ -29,6 +29,8 @@
"vite": "^6.2.6"
},
"dependencies": {
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0",
"@sveltejs/adapter-node": "^5.2.12"
}
}

View File

@@ -1,2 +1 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';

28
src/app.d.ts vendored
View File

@@ -1,13 +1,21 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
import type { Database } from './database.types.ts' // import generated types
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
namespace App {
// interface Error {}
interface Locals {
supabase: SupabaseClient<Database>
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
session: Session | null
user: User | null
}
interface PageData {
session: Session | null
}
// interface PageState {}
// interface Platform {}
}
}
export {};
export {}

View File

@@ -10,3 +10,11 @@
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
<style>
body {
font-family: "Roboto", sans-serif;
}
</style>

81
src/hooks.server.ts Normal file
View File

@@ -0,0 +1,81 @@
import { createServerClient } from '@supabase/ssr'
import { type Handle, redirect } from '@sveltejs/kit'
import { sequence } from '@sveltejs/kit/hooks'
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
const supabase: Handle = async ({ event, resolve }) => {
/**
* Creates a Supabase client specific to this server request.
*
* The Supabase client gets the Auth token from the request cookies.
*/
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
getAll: () => event.cookies.getAll(),
/**
* SvelteKit's cookies API requires `path` to be explicitly set in
* the cookie options. Setting `path` to `/` replicates previous/
* standard behavior.
*/
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
event.cookies.set(name, value, { ...options, path: '/' })
})
},
},
})
/**
* Unlike `supabase.auth.getSession()`, which returns the session _without_
* validating the JWT, this function also calls `getUser()` to validate the
* JWT before returning the session.
*/
event.locals.safeGetSession = async () => {
const {
data: { session },
} = await event.locals.supabase.auth.getSession()
if (!session) {
return { session: null, user: null }
}
const {
data: { user },
error,
} = await event.locals.supabase.auth.getUser()
if (error) {
// JWT validation has failed
return { session: null, user: null }
}
return { session, user }
}
return resolve(event, {
filterSerializedResponseHeaders(name) {
/**
* Supabase libraries use the `content-range` and `x-supabase-api-version`
* headers, so we need to tell SvelteKit to pass it through.
*/
return name === 'content-range' || name === 'x-supabase-api-version'
},
})
}
const authGuard: Handle = async ({ event, resolve }) => {
const { session, user } = await event.locals.safeGetSession()
event.locals.session = session
event.locals.user = user
if (!event.locals.session && event.url.pathname.startsWith('/private')) {
redirect(303, '/auth')
}
if (event.locals.session && event.url.pathname === '/auth') {
redirect(303, '/private/home')
}
return resolve(event)
}
export const handle: Handle = sequence(supabase, authGuard)

31
src/lib/types.ts Normal file
View File

@@ -0,0 +1,31 @@
export enum ScanState {
scanning,
scan_successful,
scan_failed
}
export type TicketData = {
id: string;
name: string;
surname: string;
email: string;
event: { id: string; name: string };
created_at: string;
created_by: { id: string; display_name: string } | null;
scanned: boolean;
scanned_at: string | null;
scanned_by: { id: string; display_name: string } | null;
};
export const defaultTicketData: TicketData = {
id: '',
name: '',
surname: '',
email: '',
event: '',
created_at: new Date().toISOString(),
created_by: null,
scanned: false,
scanned_at: null,
scanned_by: null,
};

View File

@@ -0,0 +1,9 @@
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
const { session } = await safeGetSession()
return {
session,
cookies: cookies.getAll(),
}
}

View File

@@ -1,7 +1,20 @@
<script lang="ts">
import '../app.css';
<script>
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
import "../app.css";
let { children } = $props();
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')
}
})
return () => data.subscription.unsubscribe()
})
</script>
{@render children()}

43
src/routes/+layout.ts Normal file
View File

@@ -0,0 +1,43 @@
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
import type { LayoutLoad } from './$types'
export const load: LayoutLoad = async ({ data, depends, fetch }) => {
/**
* Declare a dependency so the layout can be invalidated, for example, on
* session refresh.
*/
depends('supabase:auth')
const supabase = isBrowser()
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch,
},
})
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
global: {
fetch,
},
cookies: {
getAll() {
return data.cookies
},
},
})
/**
* It's fine to use `getSession` here, because on the client, `getSession` is
* safe, and on the server, it reads `session` from the `LayoutData`, which
* safely checked the session using `safeGetSession`.
*/
const {
data: { session },
} = await supabase.auth.getSession()
const {
data: { user },
} = await supabase.auth.getUser()
return { session, supabase, user }
}

View File

@@ -1,68 +1,6 @@
<script>
import { onMount, tick } from 'svelte';
import QRScanner from './QRScanner.svelte';
<script lang="ts">
let scanned_id = $state('');
let scan_user = $state('Roman');
let scan_data = $state({});
let ticket_state = $state('not_scanned');
onMount(() => {reset_scan_data()});
function reset_scan_data() {
ticket_state = 'unknown';
scan_data = {
id: 0,
created_at: 'none',
name: 'none',
surname: 'none',
email: 'none@esnvutbrno.cz',
uuid: 'none',
scanned: 'none',
scanned_at: 'none',
event_name: 'none',
scanned_by: 'none'
};
}
$effect(() => {
if (scan_data.scanned === true) {
ticket_state = 'Already scanned';
} else if (scan_data.scanned === false) {
ticket_state = 'Good to go in 2';
console.log(scan_data.scanned);
} else {
ticket_state = 'Ticket invalid';
}
});
$effect(() => {
console.log('Message updated:', scanned_id);
reset_scan_data();
fetch('https://n8n.orebolt.cz/webhook/9d32752c-47c9-46db-be6d-f473e97a7c25', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ scanned_id, scan_user })
})
.then((response) => response.json())
.then((data) => {
scan_data = data;
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
ticket_state = "Ticket invalid";
});
});
</script>
<QRScanner bind:message={scanned_id} />
<p>CODE: {scanned_id}</p>
<p>Name: {scan_data.name} {scan_data.surname}</p>
<p>State: {ticket_state}</p>
<p>Event: {scan_data.event_name}</p>
<p>hello world</p>

View File

@@ -0,0 +1,2 @@
<a href="/auth/login"><button>Login</button></a>
<a href="/auth/singup"><button>Signup</button></a>

View File

@@ -0,0 +1,31 @@
import type { EmailOtpType } from '@supabase/supabase-js'
import { redirect } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ url, locals: { supabase } }) => {
const token_hash = url.searchParams.get('token_hash')
const type = url.searchParams.get('type') as EmailOtpType | null
const next = url.searchParams.get('next') ?? '/'
/**
* Clean up the redirect URL by deleting the Auth flow parameters.
*
* `next` is preserved for now, because it's needed in the error case.
*/
const redirectTo = new URL(url)
redirectTo.pathname = next
redirectTo.searchParams.delete('token_hash')
redirectTo.searchParams.delete('type')
if (token_hash && type) {
const { error } = await supabase.auth.verifyOtp({ type, token_hash })
if (!error) {
redirectTo.searchParams.delete('next')
redirect(303, redirectTo)
}
}
redirectTo.pathname = '/auth/error'
redirect(303, redirectTo)
}

View File

@@ -0,0 +1 @@
<p>Login error</p>

View File

@@ -0,0 +1,41 @@
import { redirect } from '@sveltejs/kit'
import type { Actions } from './$types'
export const actions: Actions = {
signup: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const display_name = formData.get('display_name') as string
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
display_name: display_name
}
}
});
if (error) {
console.error(error)
redirect(303, '/auth/error')
} else {
redirect(303, '/')
}
},
login: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) {
console.error(error)
redirect(303, '/auth/error')
} else {
redirect(303, '/private/home')
}
},
}

View File

@@ -0,0 +1,11 @@
<form method="POST" action="?/login">
<label>
Email
<input name="email" type="email" />
</label>
<label>
Password
<input name="password" type="password" />
</label>
<button>Login</button>
</form>

View File

@@ -0,0 +1,41 @@
import { redirect } from '@sveltejs/kit'
import type { Actions } from './$types'
export const actions: Actions = {
signup: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const display_name = formData.get('display_name') as string
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
display_name: display_name
}
}
});
if (error) {
console.error(error)
redirect(303, '/auth/error')
} else {
redirect(303, '/')
}
},
login: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) {
console.error(error)
redirect(303, '/auth/error')
} else {
redirect(303, '/private/home')
}
},
}

View File

@@ -0,0 +1,15 @@
<form method="POST" action="?/login">
<label>
Email
<input name="email" type="email" />
</label>
<label>
Password
<input name="password" type="password" />
</label>
<label>
Display name
<input name="display_name" type="text" />
</label>
<button formaction="?/signup">Sign up</button>
</form>

View File

View File

@@ -0,0 +1,5 @@
/**
* This file is necessary to ensure protection of all routes in the `private`
* directory. It makes the routes in this directory _dynamic_ routes, which
* send a server request, and thus trigger `hooks.server.ts`.
**/

View File

@@ -0,0 +1,15 @@
<script lang="ts">
// Add any navbar logic here if needed
</script>
<nav class="bg-gray-800 text-white 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>
</ul>
</nav>
<div class="container p-2 bg-white ">
<slot />
</div>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation'
let { data } = $props();
let user_data = $state();
onMount(async () => {
const { data: { user } } = await data.supabase.auth.getUser();
user_data = user;
});
async function signOut() {
const { error } = await data.supabase.auth.signOut();
if (error) {
console.error('Sign out error:', error);
} else {
user_data = null; // Clear user data on sign out
await goto('/');
}
};
</script>
{#if user_data}
<div class="user-profile">
<h2 class="text-2xl font-bold mb-2">Currently logged in</h2>
<p><strong>Username:</strong> {user_data.user_metadata.display_name}</p>
<p><strong>Email:</strong> {user_data.email}</p>
</div>
<button class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 mt-4" onclick={signOut}>
Sign Out
</button>
{:else}
<p>Loading user profile...</p>
{/if}

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import QRScanner from './QRScanner.svelte';
import TicketDisplay from './TicketDisplay.svelte';
import type { TicketData } from '$lib/types';
import { ScanState, defaultTicketData } from '$lib/types';
let { data } = $props();
let scanned_id = $state<string>("");
let ticket_data = $state<TicketData>(defaultTicketData);
let scan_state = $state<ScanState>(ScanState.scanning);
$effect(() => {
if (scanned_id === "") return;
console.log('New QR code found:', scanned_id);
scan_state = ScanState.scanning;
data.supabase
.from('qrcodes')
.select(`*, event ( id, name ), scanned_by ( id, display_name )`)
.eq('id', scanned_id)
.then( response => {
if (response.data && response.data.length > 0) {
ticket_data = response.data[0];
scan_state = ScanState.scan_successful;
data.supabase.rpc('scan_ticket', { _ticket_id: scanned_id}).then();
} else {
ticket_data = defaultTicketData;
scan_state = ScanState.scan_failed;
}
})
});
</script>
<QRScanner bind:message={scanned_id} />
<TicketDisplay {ticket_data} {scan_state}/>

View File

@@ -4,8 +4,7 @@
Html5QrcodeScanner,
type Html5QrcodeResult,
Html5QrcodeScanType,
Html5QrcodeSupportedFormats,
Html5QrcodeScannerState,
Html5QrcodeSupportedFormats
} from 'html5-qrcode';
let width: number = 300;
@@ -40,7 +39,7 @@
});
</script>
<div id="qr-scanner" class="w-full max-w-sm bg-slate-700 rounded-lg overflow-hidden"></div>
<div id="qr-scanner" class="w-full h-full max-w-none bg-slate-700 overflow-hidden"></div>
<style>
/* Hide unwanted icons */
@@ -49,6 +48,10 @@
display: none;
}
#qr-scanner {
color: white !important;
}
/* Change camera permission button text */
#qr-scanner :global(#html5-qrcode-button-camera-permission) {
visibility: hidden;

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import type { TicketData } from '$lib/types';
import { ScanState } from '$lib/types';
let { ticket_data, scan_state }: { ticket_data: TicketData; scan_state: ScanState } = $props();
function formatScannedAt(dateString: string): string {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}.${month}. ${hours}:${minutes}`;
}
</script>
<div class="py-3">
{#if scan_state === ScanState.scanning}
<div class="rounded border-l-4 border-orange-500 bg-orange-100 p-4 text-orange-700">
<p>Waiting for data...</p>
</div>
{:else if scan_state === ScanState.scan_failed}
<div class="rounded border-l-4 border-green-500 bg-green-100 p-4 text-green-700">
<p>Scan failed. Please try again.</p>
</div>
{:else if scan_state === ScanState.scan_successful}
{#if ticket_data.scanned}
<div class="rounded border-l-4 border-red-500 bg-red-100 p-4 text-red-700">
<p>Ticket already scanned!</p>
<p>
By {ticket_data.scanned_by?.display_name} on
{formatScannedAt(ticket_data.scanned_at)}
</p>
</div>
{:else}
<div class="rounded border-l-4 border-green-500 bg-green-100 p-4 text-green-700">
<ol>
<li>{ticket_data.event.name}</li>
<li>{ticket_data.name} {ticket_data.surname}</li>
</ol>
</div>
{/if}
{/if}
</div>

View File

@@ -0,0 +1 @@
<p>setup</p>