Compare commits
6 Commits
51dbfcc1bb
...
2c11e82424
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c11e82424 | ||
|
|
21ed61f0c2 | ||
|
|
b6d9b8df44 | ||
|
|
a135c5ac36 | ||
|
|
58872bada6 | ||
|
|
9c94f9c717 |
54
package-lock.json
generated
54
package-lock.json
generated
@@ -8,7 +8,8 @@
|
||||
"name": "esn-code-scanner",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.50.0",
|
||||
"@sveltejs/adapter-node": "^5.2.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -869,9 +870,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.69.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz",
|
||||
"integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==",
|
||||
"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"
|
||||
@@ -908,15 +909,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz",
|
||||
"integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==",
|
||||
"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.14",
|
||||
"@types/phoenix": "^1.5.4",
|
||||
"@types/ws": "^8.5.10",
|
||||
"ws": "^8.18.0"
|
||||
"@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": {
|
||||
@@ -929,16 +951,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.49.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz",
|
||||
"integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==",
|
||||
"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.69.1",
|
||||
"@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.2",
|
||||
"@supabase/realtime-js": "2.11.10",
|
||||
"@supabase/storage-js": "2.7.1"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"vite": "^6.2.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.50.0",
|
||||
"@sveltejs/adapter-node": "^5.2.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/typography';
|
||||
|
||||
18
src/app.d.ts
vendored
18
src/app.d.ts
vendored
@@ -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 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 {}
|
||||
@@ -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
81
src/hooks.server.ts
Normal 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
31
src/lib/types.ts
Normal 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,
|
||||
};
|
||||
9
src/routes/+layout.server.ts
Normal file
9
src/routes/+layout.server.ts
Normal 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(),
|
||||
}
|
||||
}
|
||||
@@ -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()}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
|
||||
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
|
||||
import type { LayoutLoad } from './$types'
|
||||
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
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: {
|
||||
@@ -16,10 +21,11 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
},
|
||||
cookies: {
|
||||
getAll() {
|
||||
return data?.cookies ?? []
|
||||
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
|
||||
@@ -28,5 +34,10 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
return { supabase, session }
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
return { session, supabase, user }
|
||||
}
|
||||
@@ -1,15 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let data = $props();
|
||||
|
||||
onMount( async () => {
|
||||
await handleClick();
|
||||
});
|
||||
|
||||
async function handleClick() {
|
||||
console.log( await data.data.supabase.from('qrcodes').select().eq('id', "4b461fd7-d7db-4739-8ca0-6e78cc299813"));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
2
src/routes/auth/+page.svelte
Normal file
2
src/routes/auth/+page.svelte
Normal file
@@ -0,0 +1,2 @@
|
||||
<a href="/auth/login"><button>Login</button></a>
|
||||
<a href="/auth/singup"><button>Signup</button></a>
|
||||
31
src/routes/auth/confirm/+server.ts
Normal file
31
src/routes/auth/confirm/+server.ts
Normal 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)
|
||||
}
|
||||
1
src/routes/auth/error/+page.svelte
Normal file
1
src/routes/auth/error/+page.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<p>Login error</p>
|
||||
41
src/routes/auth/login/+page.server.ts
Normal file
41
src/routes/auth/login/+page.server.ts
Normal 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')
|
||||
}
|
||||
},
|
||||
}
|
||||
11
src/routes/auth/login/+page.svelte
Normal file
11
src/routes/auth/login/+page.svelte
Normal 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>
|
||||
41
src/routes/auth/signup/+page.server.ts
Normal file
41
src/routes/auth/signup/+page.server.ts
Normal 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')
|
||||
}
|
||||
},
|
||||
}
|
||||
15
src/routes/auth/signup/+page.svelte
Normal file
15
src/routes/auth/signup/+page.svelte
Normal 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>
|
||||
0
src/routes/login/+page.svelte
Normal file
0
src/routes/login/+page.svelte
Normal file
5
src/routes/private/+layout.server.ts
Normal file
5
src/routes/private/+layout.server.ts
Normal 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`.
|
||||
**/
|
||||
15
src/routes/private/+layout.svelte
Normal file
15
src/routes/private/+layout.svelte
Normal 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>
|
||||
36
src/routes/private/home/+page.svelte
Normal file
36
src/routes/private/home/+page.svelte
Normal 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}
|
||||
37
src/routes/private/scanner/+page.svelte
Normal file
37
src/routes/private/scanner/+page.svelte
Normal 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}/>
|
||||
@@ -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;
|
||||
44
src/routes/private/scanner/TicketDisplay.svelte
Normal file
44
src/routes/private/scanner/TicketDisplay.svelte
Normal 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>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script>
|
||||
import { onMount, tick } from 'svelte';
|
||||
import QRScanner from './QRScanner.svelte';
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user