Working
This commit is contained in:
54
package-lock.json
generated
54
package-lock.json
generated
@@ -8,7 +8,8 @@
|
|||||||
"name": "esn-code-scanner",
|
"name": "esn-code-scanner",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/ssr": "^0.6.1",
|
||||||
|
"@supabase/supabase-js": "^2.50.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12"
|
"@sveltejs/adapter-node": "^5.2.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -869,9 +870,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@supabase/auth-js": {
|
"node_modules/@supabase/auth-js": {
|
||||||
"version": "2.69.1",
|
"version": "2.70.0",
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz",
|
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz",
|
||||||
"integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==",
|
"integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/node-fetch": "^2.6.14"
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
@@ -908,15 +909,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@supabase/realtime-js": {
|
"node_modules/@supabase/realtime-js": {
|
||||||
"version": "2.11.2",
|
"version": "2.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz",
|
||||||
"integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==",
|
"integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/node-fetch": "^2.6.14",
|
"@supabase/node-fetch": "^2.6.13",
|
||||||
"@types/phoenix": "^1.5.4",
|
"@types/phoenix": "^1.6.6",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.18.1",
|
||||||
"ws": "^8.18.0"
|
"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": {
|
"node_modules/@supabase/storage-js": {
|
||||||
@@ -929,16 +951,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@supabase/supabase-js": {
|
"node_modules/@supabase/supabase-js": {
|
||||||
"version": "2.49.4",
|
"version": "2.50.0",
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz",
|
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz",
|
||||||
"integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==",
|
"integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/auth-js": "2.69.1",
|
"@supabase/auth-js": "2.70.0",
|
||||||
"@supabase/functions-js": "2.4.4",
|
"@supabase/functions-js": "2.4.4",
|
||||||
"@supabase/node-fetch": "2.6.15",
|
"@supabase/node-fetch": "2.6.15",
|
||||||
"@supabase/postgrest-js": "1.19.4",
|
"@supabase/postgrest-js": "1.19.4",
|
||||||
"@supabase/realtime-js": "2.11.2",
|
"@supabase/realtime-js": "2.11.10",
|
||||||
"@supabase/storage-js": "2.7.1"
|
"@supabase/storage-js": "2.7.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
"vite": "^6.2.6"
|
"vite": "^6.2.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/ssr": "^0.6.1",
|
||||||
|
"@supabase/supabase-js": "^2.50.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12"
|
"@sveltejs/adapter-node": "^5.2.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@plugin '@tailwindcss/typography';
|
|
||||||
|
body {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
}
|
||||||
21
src/app.d.ts
vendored
21
src/app.d.ts
vendored
@@ -1,16 +1,21 @@
|
|||||||
// src/app.d.ts
|
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
|
||||||
import { SupabaseClient, Session } from '@supabase/supabase-js'
|
import type { Database } from './database.types.ts' // import generated types
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
supabase: SupabaseClient
|
supabase: SupabaseClient<Database>
|
||||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
|
||||||
}
|
|
||||||
interface PageData {
|
|
||||||
session: Session | null
|
session: Session | null
|
||||||
user: User | null
|
user: User | null
|
||||||
}
|
}
|
||||||
// interface Error {}
|
interface PageData {
|
||||||
|
session: Session | null
|
||||||
|
}
|
||||||
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
// src/hooks.server.ts
|
|
||||||
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
|
|
||||||
import { createServerClient } from '@supabase/ssr'
|
import { createServerClient } from '@supabase/ssr'
|
||||||
import type { Handle } from '@sveltejs/kit'
|
import { type Handle, redirect } from '@sveltejs/kit'
|
||||||
|
import { sequence } from '@sveltejs/kit/hooks'
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
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, {
|
event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||||
cookies: {
|
cookies: {
|
||||||
getAll: () => event.cookies.getAll(),
|
getAll: () => event.cookies.getAll(),
|
||||||
@@ -47,7 +53,29 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
|
|
||||||
return resolve(event, {
|
return resolve(event, {
|
||||||
filterSerializedResponseHeaders(name) {
|
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'
|
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)
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
// src/routes/+layout.server.ts
|
|
||||||
import type { LayoutServerLoad } from './$types'
|
import type { LayoutServerLoad } from './$types'
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
||||||
const { session, user } = await safeGetSession()
|
const { session } = await safeGetSession()
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
user,
|
|
||||||
cookies: cookies.getAll(),
|
cookies: cookies.getAll(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
<script lang="ts">
|
<script>
|
||||||
import '../app.css';
|
import { invalidate } from '$app/navigation'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
|
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'
|
||||||
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
|
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
|
||||||
import type { LayoutLoad } from './$types'
|
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')
|
depends('supabase:auth')
|
||||||
|
|
||||||
const supabase = isBrowser()
|
const supabase = isBrowser()
|
||||||
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||||
global: {
|
global: {
|
||||||
fetch,
|
fetch,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
: createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||||
global: {
|
global: {
|
||||||
@@ -16,10 +21,11 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
|||||||
},
|
},
|
||||||
cookies: {
|
cookies: {
|
||||||
getAll() {
|
getAll() {
|
||||||
return data.cookies ?? []
|
return data.cookies
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
* 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
|
* 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 {
|
const {
|
||||||
data: { session },
|
data: { session },
|
||||||
} = await supabase.auth.getSession()
|
} = await supabase.auth.getSession()
|
||||||
return { supabase, session }
|
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabase.auth.getUser()
|
||||||
|
|
||||||
|
return { session, supabase, user }
|
||||||
}
|
}
|
||||||
11
src/routes/auth/+layout.svelte
Normal file
11
src/routes/auth/+layout.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
let { children } = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
32
src/routes/auth/+page.server.ts
Normal file
32
src/routes/auth/+page.server.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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 { error } = await supabase.auth.signUp({ email, password })
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
12
src/routes/auth/+page.svelte
Normal file
12
src/routes/auth/+page.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<form method="POST" action="?/login">
|
||||||
|
<label>
|
||||||
|
Email
|
||||||
|
<input name="email" type="email" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Password
|
||||||
|
<input name="password" type="password" />
|
||||||
|
</label>
|
||||||
|
<button>Login</button>
|
||||||
|
<button formaction="?/signup">Sign up</button>
|
||||||
|
</form>
|
||||||
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>
|
||||||
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`.
|
||||||
|
**/
|
||||||
6
src/routes/private/home/+page.svelte
Normal file
6
src/routes/private/home/+page.svelte
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
heyy
|
||||||
@@ -27,6 +27,12 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.robo {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<QRScanner bind:message={scanned_id} />
|
<QRScanner bind:message={scanned_id} />
|
||||||
|
|
||||||
{#if scan_state === ScanState.scan_successful}
|
{#if scan_state === ScanState.scan_successful}
|
||||||
@@ -34,9 +40,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if scan_state === ScanState.scan_failed}
|
{#if scan_state === ScanState.scan_failed}
|
||||||
<p>Scan failed. Please try again.</p>
|
<p class="robo">Scan failed. Please try again.</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if scan_state === ScanState.scanning}
|
{#if scan_state === ScanState.scanning}
|
||||||
<p>Fetching data...</p>
|
<p class="robo">Fetching data...</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
Html5QrcodeScanner,
|
Html5QrcodeScanner,
|
||||||
type Html5QrcodeResult,
|
type Html5QrcodeResult,
|
||||||
Html5QrcodeScanType,
|
Html5QrcodeScanType,
|
||||||
Html5QrcodeSupportedFormats,
|
Html5QrcodeSupportedFormats
|
||||||
Html5QrcodeScannerState,
|
|
||||||
} from 'html5-qrcode';
|
} from 'html5-qrcode';
|
||||||
|
|
||||||
let width: number = 300;
|
let width: number = 300;
|
||||||
|
|||||||
@@ -4,5 +4,11 @@
|
|||||||
let { ticket_data }: { ticket_data: TicketData } = $props();
|
let { ticket_data }: { ticket_data: TicketData } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p>{ticket_data.name}</p>
|
<p class="robo">{ticket_data.name}</p>
|
||||||
<p>{ticket_data.surname}</p>
|
<p>{ticket_data.surname}</p>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.robo {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user