14 Commits

Author SHA1 Message Date
621d2bff2d Merge pull request 'Styling of creator progress' (#7) from supabase into main
All checks were successful
Build Docker image / build (push) Successful in 59s
Build Docker image / deploy (push) Successful in 2s
Build Docker image / verify (push) Successful in 23s
Reviewed-on: erman/esn-code-scanner-app#7
2025-06-24 12:45:39 +02:00
f161aa0a3a Merge pull request 'supabase' (#6) from supabase into main
All checks were successful
Build Docker image / build (push) Successful in 1m17s
Build Docker image / deploy (push) Successful in 12s
Build Docker image / verify (push) Successful in 31s
Reviewed-on: erman/esn-code-scanner-app#6
2025-06-24 11:29:07 +02:00
617c00e8dc Add deploy and verify
All checks were successful
Build Docker image / build (push) Successful in 2m23s
Build Docker image / deploy (push) Successful in 2s
Build Docker image / verify (push) Successful in 37s
2025-06-24 11:25:40 +02:00
2ca7dc72cb Testing komodo CI
Some checks failed
Build Docker image / build (push) Failing after 5s
2025-06-24 11:15:15 +02:00
d8d2269817 Merge pull request 'Fix google env variables' (#5) from supabase into main
All checks were successful
Build Docker image / build (push) Successful in 1m25s
Reviewed-on: erman/esn-code-scanner-app#5
2025-06-22 17:59:20 +02:00
f768ae8d8b Fix double runs
All checks were successful
Build Docker image / build (push) Successful in 1m28s
2025-06-22 17:52:57 +02:00
e2a5fe2190 Merge pull request 'Add mailing prototype' (#4) from supabase into main
Some checks failed
Build Docker image / build (push) Has been cancelled
Reviewed-on: erman/esn-code-scanner-app#4
2025-06-22 17:48:47 +02:00
1ffe7d862f Merge pull request 'Styling' (#3) from supabase into main
All checks were successful
Build Docker image / build (push) Successful in 1m49s
Reviewed-on: erman/esn-code-scanner-app#3
2025-06-21 23:36:53 +02:00
fb9a6677e1 Run only on merges
Some checks failed
Build Docker image / build (push) Has been cancelled
2025-06-21 23:36:20 +02:00
aba3369565 Merge pull request 'Fix missing env variable during build time' (#2) from supabase into main
All checks were successful
Build Docker image / build (push) Successful in 2m49s
Reviewed-on: erman/esn-code-scanner-app#2
2025-06-21 22:40:33 +02:00
083a7ce2e5 Don't ignore CI changes
Some checks failed
Build Docker image / build (push) Failing after 5m27s
2025-06-21 22:15:54 +02:00
2bd7edde17 Run actions on closed pulls 2025-06-21 22:13:19 +02:00
4dd35c64e0 Merge pull request 'supabase' (#1) from supabase into main
Reviewed-on: erman/esn-code-scanner-app#1
2025-06-21 22:10:08 +02:00
2bf0394ffc Build release update only once a month
Some checks failed
Build Docker image / build (push) Failing after 5s
2025-05-29 11:08:42 +02:00
28 changed files with 204 additions and 1232 deletions

View File

@@ -5,7 +5,7 @@ on:
branches:
- main
schedule:
- cron: "0 22 * * 0" # sunday 22:00
- cron: "0 22 1 * *" # First of every month
jobs:
build:
@@ -46,8 +46,28 @@ 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=ScanWave
org.opencontainers.image.title=ESN Code Scanner App
deploy:
needs: build
steps:
- name: Trigger Komodo Deploy
env:
URL: ${{ secrets.KOMODO_URL }}
SECRET: ${{ secrets.KOMODO_SECRET }}
BODY_FILE: ${{ github.event_path }}
run: |
SIG="sha256=$(openssl dgst -sha256 -hmac "$SECRET" "$BODY_FILE" | cut -d' ' -f2)"
curl -fsSL -X POST "$URL" \
-H 'Content-Type: application/json' \
-H "X-Hub-Signature-256: $SIG" \
-H 'X-GitHub-Event: push' \
-H "X-GitHub-Delivery: $GITHUB_RUN_ID.$GITHUB_RUN_NUMBER" \
--data @"$BODY_FILE"
verify:
needs: build
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.24.0
with:

View File

@@ -1,12 +1,12 @@
services:
app:
image: ${DOCKER_REGISTRY}/${DOCKER_USER}/${DOCKER_IMAGE}:latest
image: ${DOCKER_REGISTRY}/${DOCKER_USER}/esn-code-scanner-app: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"
- "traefik.http.routers.esn-scanner.rule=Host(`scanner.esn.orebolt.cz`)"
- "traefik.http.routers.esn-scanner.tls.certresolver=leresolver"
- "traefik.http.routers.esn-scanner.entrypoints=websecure"
- "traefik.http.services.esn-scanner.loadbalancer.server.port=3000"
- "traefik.http.routers.esn-scanner.middlewares=hsts"

View File

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

448
package-lock.json generated
View File

@@ -1,24 +1,21 @@
{
"name": "scan-wave",
"name": "esn-code-scanner",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scan-wave",
"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",
"googleapis": "^150.0.1",
"papaparse": "^5.5.3",
"qrcode": "^1.5.4",
"quoted-printable": "^1.0.1",
"simple-icons": "^15.3.0"
"papaparse": "^5.5.3"
},
"devDependencies": {
"@sveltejs/kit": "^2.22.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
@@ -30,7 +27,7 @@
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite-plugin-devtools-json": "^0.2.0"
"vite": "^6.2.6"
}
},
"node_modules/@ampproject/remapping": {
@@ -58,7 +55,6 @@
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -75,7 +71,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -92,7 +87,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -109,7 +103,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -126,7 +119,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -143,7 +135,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -160,7 +151,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -177,7 +167,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -194,7 +183,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -211,7 +199,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -228,7 +215,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -245,7 +231,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -262,7 +247,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -279,7 +263,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -296,7 +279,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -313,7 +295,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -330,7 +311,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -347,7 +327,6 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -364,7 +343,6 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -381,7 +359,6 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -398,7 +375,6 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -415,7 +391,6 @@
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -432,7 +407,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -449,7 +423,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -466,7 +439,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1019,15 +991,15 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.0.tgz",
"integrity": "sha512-DJm0UxVgzXq+1MUfiJK4Ridk7oIQsIets6JwHiEl97sI6nXScfXe+BeqNhzB7jQIVBb3BM51U4hNk8qQxRXBAA==",
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.21.0.tgz",
"integrity": "sha512-kvu4h9qXduiPk1Q1oqFKDLFGu/7mslEYbVaqpbBcBxjlRJnvNCFwEvEwKt0Mx9TtSi8J77xRelvJobrGlst4nQ==",
"license": "MIT",
"dependencies": {
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/cookie": "^0.6.0",
"acorn": "^8.14.1",
"cookie": "^0.6.0",
"cookie": "^0.7.0",
"devalue": "^5.1.0",
"esm-env": "^1.2.2",
"kleur": "^4.1.5",
@@ -1035,8 +1007,7 @@
"mrmime": "^2.0.0",
"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
"sirv": "^3.0.0",
"vitefu": "^1.0.6"
"sirv": "^3.0.0"
},
"bin": {
"svelte-kit": "svelte-kit.js"
@@ -1045,9 +1016,9 @@
"node": ">=18.13"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
"vite": "^5.0.3 || ^6.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
@@ -1444,30 +1415,6 @@
"node": ">= 14"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -1550,15 +1497,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1585,17 +1523,6 @@
"node": ">=18"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -1605,24 +1532,6 @@
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -1630,9 +1539,9 @@
"license": "MIT"
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -1677,15 +1586,6 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -1711,12 +1611,6 @@
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"license": "MIT"
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1740,12 +1634,6 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -1796,7 +1684,6 @@
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -1895,19 +1782,6 @@
"node": "^12.20 || >= 14.13"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -1971,15 +1845,6 @@
"node": ">=18"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -2164,15 +2029,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
@@ -2472,18 +2328,6 @@
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
@@ -2597,7 +2441,6 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -2655,57 +2498,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/papaparse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -2730,15 +2528,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@@ -2758,7 +2547,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -2888,23 +2676,6 @@
}
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -2920,18 +2691,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/quoted-printable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/quoted-printable/-/quoted-printable-1.0.1.tgz",
"integrity": "sha512-cihC68OcGiQOjGiXuo5Jk6XHANTHl1K4JLk/xlEJRTIXfy19Sg6XzB95XonYgr+1rB88bCpr7WZE7D7AlZow4g==",
"license": "MIT",
"dependencies": {
"utf8": "^2.1.0"
},
"bin": {
"quoted-printable": "bin/quoted-printable"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2946,21 +2705,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -3052,12 +2796,6 @@
],
"license": "MIT"
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
@@ -3136,25 +2874,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/simple-icons": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-15.3.0.tgz",
"integrity": "sha512-wcaQWT4FOMQ59qe+9zNJpby286UPr8rNDuRzlXzBSu/cKECEzwvCFgCmaWUNGbpBzjeYk6WykzmOmAyGKTe/Kg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/simple-icons"
},
{
"type": "github",
"url": "https://github.com/sponsors/simple-icons"
}
],
"license": "CC0-1.0",
"engines": {
"node": ">=0.12.18"
}
},
"node_modules/sirv": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
@@ -3178,32 +2897,6 @@
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -3265,13 +2958,6 @@
"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",
@@ -3312,7 +2998,6 @@
"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"
@@ -3339,14 +3024,6 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"optional": true
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
@@ -3373,12 +3050,6 @@
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
"license": "BSD"
},
"node_modules/utf8": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
"integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3386,26 +3057,11 @@
"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",
@@ -3475,19 +3131,6 @@
}
}
},
"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",
@@ -3531,26 +3174,6 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
@@ -3572,12 +3195,6 @@
}
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -3588,41 +3205,6 @@
"node": ">=18"
}
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/zimmerframe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",

View File

@@ -1,5 +1,5 @@
{
"name": "scan-wave",
"name": "esn-code-scanner",
"private": true,
"version": "0.0.1",
"type": "module",
@@ -14,7 +14,7 @@
"lint": "prettier --check ."
},
"devDependencies": {
"@sveltejs/kit": "^2.22.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
@@ -26,16 +26,13 @@
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vite-plugin-devtools-json": "^0.2.0"
"vite": "^6.2.6"
},
"dependencies": {
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.0",
"@sveltejs/adapter-node": "^5.2.12",
"googleapis": "^150.0.1",
"papaparse": "^5.5.3",
"qrcode": "^1.5.4",
"quoted-printable": "^1.0.1",
"simple-icons": "^15.3.0"
"papaparse": "^5.5.3"
}
}

View File

@@ -1,6 +1,5 @@
import { google } from 'googleapis';
import { env } from '$env/dynamic/private';
import quotedPrintable from 'quoted-printable'; // tiny, zero-dep package
export const scopes = ['https://www.googleapis.com/auth/gmail.send'];
@@ -28,73 +27,21 @@ export async function exchangeCodeForTokens(code: string) {
export async function sendGmail(
refreshToken: string,
{ to, subject, text, qr_code }: { to: string; subject: string; text: string; qr_code: string }
{ to, subject, text }: { to: string; subject: string; text: string }
) {
const oauth = getOAuthClient();
oauth.setCredentials({ refresh_token: refreshToken });
const gmail = google.gmail({ version: 'v1', auth: oauth });
const message_html =
`<!DOCTYPE html>
<html lang="en">
<head>
<style>
@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap');
</style>
</head>
<body style="font-family: 'Lato', sans-serif; background-color: #f9f9f9; padding: 20px; margin: 0;">
<div style="max-width: 600px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
<p style="white-space: pre-line;font-size: 16px; line-height: 1.5; color: #333;">${text}</p>
<img src="cid:qrCode1" alt="QR Code" style="display: block; margin: 20px auto; max-width: 50%; min-width: 200px; height: auto;" />
<div style="width: 100%; display: flex; flex-direction: row; justify-content: space-between">
<div style="height: 4px; width: 20%; background: #00aeef;"></div>
<div style="height: 4px; width: 20%; background: #ec008c;"></div>
<div style="height: 4px; width: 20%; background: #7ac143;"></div>
<div style="height: 4px; width: 20%; background: #f47b20;"></div>
<div style="height: 4px; width: 20%; background: #2e3192;"></div>
</div>
<div style="font-size: 12px; color: #999; padding-top: 0px; margin-top: 10px; line-height: 1.5; ">
<p>This email has been generated with the help of *insert software name*</p>
</div>
</div>
</body>
</html>`;
const boundary = 'BOUNDARY';
const nl = '\r\n'; // RFC-5322 line ending
const htmlQP = quotedPrintable.encode(message_html);
const qrLines = qr_code.replace(/.{1,76}/g, '$&' + nl);
const rawParts = [
'MIME-Version: 1.0',
`To: ${to}`,
const raw = Buffer
.from(
[`To: ${to}`,
'Content-Type: text/plain; charset="UTF-8"',
'Content-Transfer-Encoding: 7bit',
`Subject: ${subject}`,
`Content-Type: multipart/related; boundary="${boundary}"`,
'',
`--${boundary}`,
'Content-Type: text/html; charset="UTF-8"',
'Content-Transfer-Encoding: quoted-printable',
'',
htmlQP,
'',
`--${boundary}`,
'Content-Type: image/png',
'Content-Transfer-Encoding: base64',
'Content-ID: <qrCode1>',
'Content-Disposition: inline; filename="qr.png"',
'',
qrLines,
'',
`--${boundary}--`,
''
];
const rawMessage = rawParts.join(nl);
const raw = Buffer.from(rawMessage).toString('base64url');
text].join('\n'))
.toString('base64url');
await gmail.users.messages.send({
userId: 'me',

View File

@@ -1,24 +1,20 @@
<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');
invalidate('supabase:auth')
}
});
})
return () => data.subscription.unsubscribe();
});
return () => data.subscription.unsubscribe()
})
</script>
<svelte:head>
<title>ScanWave</title>
</svelte:head>
{@render children()}

View File

@@ -3,11 +3,9 @@
<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">ScanWave</h1>
<h1 class="text-3xl font-bold text-center mb-2">ESN Scanner App</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="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>
<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>
</div>
</div>

View File

@@ -2,20 +2,15 @@
// Add any navbar logic here if needed
</script>
<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>
<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>
<li><a href="/private/creator" class="hover:underline">Creator</a></li>
</ul>
</div>
</div>
</nav>
<div class="container max-w-2xl mx-auto p-2 bg-white">
<div class="container p-2 bg-white ">
<slot />
</div>

View File

@@ -36,13 +36,13 @@ export const GET: RequestHandler = async ({ url }) => {
/* ───────────── POST ───────────── */
export const POST: RequestHandler = async ({ request }) => {
const { action, refreshToken, to, subject, text, qr_code } = await request.json();
const { action, refreshToken, to, subject, text } = await request.json();
/* send e-mail */
if (action === 'send') {
if (!refreshToken) return new Response('Missing token', { status: 401 });
try {
await sendGmail(refreshToken, { to, subject, text, qr_code });
await sendGmail(refreshToken, { to, subject, text });
return json({ ok: true });
} catch (err) {
return new Response((err as Error).message, { status: 500 });

View File

@@ -4,6 +4,8 @@ import Papa from 'papaparse';
import { fail } from '@sveltejs/kit';
export async function load({ locals }) {
console.log("▶️ fetching events…");
const { data: events, error } = await locals.supabase
.from('events')
.select('*')
@@ -21,6 +23,8 @@ export async function load({ locals }) {
export const actions = {
create: async (event) => {
const formData = await event.request.formData();
console.log(Array.from(formData.entries()));
console.log('create_event date', formData.get("date"), formData.get("name"), formData.get("description"));
let { data: new_event, error } = await event.locals.supabase.rpc("create_event",
{
"p_name": formData.get('name'),
@@ -47,6 +51,8 @@ export const actions = {
parsedRows.shift();
}
console.log('Parsed rows:', parsedRows);
// Map each row to an object with keys: name, surname, email
const participants = parsedRows.map((row: string[]) => ({
name: row[0],
@@ -54,6 +60,8 @@ export const actions = {
email: row[2]
}));
console.log('Mapped participants:', participants);
return {
participants,
}

View File

@@ -7,14 +7,15 @@
let { data, form } = $props();
let event = $state({});
let new_event = $state({});
let participants = $state([]);
let email = $state({'body': '', 'subject': ''});
let authorized = $state(false);
let subject = $state('');
let body = $state('');
$effect(() => {
// Update events and participants from the form data
$effect( () => {
if (form && form.new_event) {
event = form.new_event;
new_event = form.new_event;
}
if (form && form.participants) {
participants = form.participants;
@@ -30,34 +31,36 @@
StepOverview
];
let step: number = $state(0);
// let stepConditions = $derived([
// authorized,
// !!new_event?.name,
// !!participants?.length,
// !!subject && !!body
// ]);
// for debugging purpouses
let stepConditions = [
true,
true,
true,
true
];
// State variable for current step
let step = $state(0);
// Helper to go to next/previous step (optional)
function nextStep() {
if (step < steps.length - 1) step += 1;
console.log(step);
}
function prevStep() {
if (step > 0) step -= 1;
console.log(step);
}
</script>
<div class="flex items-center justify-between mb-4 mt-2">
<!-- Render the current step component -->
{#if step == 0}
<StepConnectGoogle />
{:else if step == 1}
<StepCreateEvent events={data.events} {new_event} />
{:else if step == 2}
<StepUploadFiles {participants} />
{:else if step == 3}
<StepCraftEmail bind:subject bind:body />
{:else if step == 4}
<StepOverview {new_event} {participants} {subject} {body} />
{/if}
<div class="flex items-center justify-between mt-4">
<button
onclick={prevStep}
on:click={prevStep}
disabled={step === 0}
class="min-w-[100px] py-2 px-4 bg-white border border-gray-300 text-gray-700 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition"
>
@@ -67,28 +70,10 @@
Step {step + 1} of {steps.length}
</span>
<button
onclick={nextStep}
disabled={step === steps.length - 1 || !stepConditions[step]}
on:click={nextStep}
disabled={step === steps.length - 1}
class="min-w-[100px] py-2 px-4 bg-white border border-gray-300 text-gray-700 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition"
>
Next
</button>
</div>
{#if step == 0}
<StepConnectGoogle bind:authorized />
{:else if step == 1}
<StepCreateEvent {event} />
{:else if step == 2}
<StepUploadFiles {participants} />
{:else if step == 3}
<StepCraftEmail bind:email />
{:else if step == 4}
<StepOverview
{data}
{event}
{participants}
{email}
{stepConditions}
/>
{/if}

View File

@@ -1,110 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
let to = '';
let subject = '';
let body = '';
let qrcode_b64 = '';
let loading = false;
let error = '';
let success = '';
let authorized = false;
let refreshToken = '';
async function validateToken(token: string): Promise<boolean> {
if (!token) return false;
const res = await fetch('/private/api/gmail', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'validate', refreshToken: token })
});
if (!res.ok) return false;
const data = await res.json();
return !!data.valid;
}
onMount(async () => {
refreshToken = localStorage.getItem('gmail_refresh_token') ?? '';
authorized = await validateToken(refreshToken);
});
const connect = () => goto('/private/api/gmail?action=auth');
async function sendTestEmail() {
error = '';
success = '';
loading = true;
try {
const r = await fetch('/private/api/gmail', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'send',
to,
subject,
text: body,
qr_code: qrcode_b64,
refreshToken
})
});
if (r.ok) {
success = 'Email sent!';
to = subject = body = qrcode_b64 = '';
} else {
error = await r.text();
}
} catch (e) {
error = e.message || 'Unknown error';
}
loading = false;
}
</script>
<div class="max-w-lg mx-auto bg-white border border-gray-300 rounded p-8 mt-8 shadow">
<h2 class="text-2xl font-semibold mb-6 text-center">Test Email Sender</h2>
{#if !authorized}
<div class="mb-4 flex items-center justify-between">
<p class="text-gray-700">Google not connected.</p>
<button class="btn bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded ml-auto" on:click={connect}>
Connect Google
</button>
</div>
{:else}
<form on:submit|preventDefault={sendTestEmail} class="space-y-4">
<label class="block">
<span class="text-gray-700">To</span>
<input type="email" class="mt-1 block w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-200" bind:value={to} required />
</label>
<label class="block">
<span class="text-gray-700">Subject</span>
<input type="text" class="mt-1 block w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-200" bind:value={subject} required />
</label>
<label class="block">
<span class="text-gray-700">Body</span>
<textarea class="mt-1 block w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-200 resize-none" rows="6" bind:value={body} required></textarea>
</label>
<label class="block">
<span class="text-gray-700">QR Code (base64, data:image/png;base64,...)</span>
<input type="text" class="mt-1 block w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-200 font-mono text-xs" bind:value={qrcode_b64} placeholder="Paste base64 image string here" required />
</label>
<button type="submit" class="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition" disabled={loading}>
{#if loading}
<svg class="animate-spin h-5 w-5 mr-2 inline-block text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
Sending...
{:else}
Send Test Email
{/if}
</button>
</form>
{/if}
{#if error}
<div class="rounded border-l-4 border-red-500 bg-red-100 p-4 text-red-700 mt-4">{error}</div>
{/if}
{#if success}
<div class="rounded border-l-4 border-green-500 bg-green-100 p-4 text-green-700 mt-4">{success}</div>
{/if}
</div>

View File

@@ -1,125 +0,0 @@
<script lang="ts">
import { page } from '$app/state';
import { onMount } from 'svelte';
import QRCode from 'qrcode';
let { data } = $props();
let session_storage_id = page.url.searchParams.get('data');
let all_data = {};
const StepStatus = {
Loading: 'loading',
Waiting: 'waiting',
Success: 'success',
Failure: 'failure'
} as const;
type StepStatus = (typeof StepStatus)[keyof typeof StepStatus];
let supabase_status: StepStatus = $state(StepStatus.Waiting);
let email_status: StepStatus = $state(StepStatus.Waiting);
onMount(async () => {
if (!session_storage_id) {
console.error('No session storage ID provided in the URL');
return;
}
all_data = JSON.parse(sessionStorage.getItem(session_storage_id) || '{}');
supabase_status = StepStatus.Loading;
try {
const { result } = await insert_data_supabase(all_data.participants, all_data.event);
supabase_status = StepStatus.Success;
// Now send emails
email_status = StepStatus.Loading;
let allSuccess = true;
for (const obj of result) {
let qr_code = await dataToBase64(obj.id);
const payload = {
action: 'send',
to: obj.email,
subject: all_data.email.subject,
text: all_data.email.body,
qr_code: qr_code,
refreshToken: localStorage.getItem('gmail_refresh_token')
};
const res = await fetch('/private/api/gmail', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
allSuccess = false;
console.error('Failed to send email to', obj.email, await res.text());
}
}
email_status = allSuccess ? StepStatus.Success : StepStatus.Failure;
} catch (e) {
supabase_status = StepStatus.Failure;
email_status = StepStatus.Failure;
console.error(e);
}
});
async function dataToBase64(data: string): Promise<string> {
try {
const url = await QRCode.toDataURL(data);
const parts = url.split(',');
const base64 = parts[1];
return base64;
} catch (err) {
console.error(err);
return '';
}
}
async function insert_data_supabase(participants, event) {
const names = participants.map((p) => p.name);
const surnames = participants.map((p) => p.surname);
const emails = participants.map((p) => p.email);
const {
data: { user },
error: authError
} = await data.supabase.auth.getUser();
const { data: user_profile, error: profileError } = await data.supabase
.from('profiles')
.select('*, section:sections (id, name)')
.eq('id', user?.id)
.single();
const { data: result, error: qrCodeError } = await data.supabase.rpc('create_qrcodes_bulk', {
p_section_id: user_profile?.section.id,
p_event_id: event.id,
p_names: names,
p_surnames: surnames,
p_emails: emails
});
return { result };
}
</script>
<!-- Creating Database Entries -->
<div class="mb-4 rounded border border-gray-300 bg-white p-4">
<h2 class="mb-2 text-xl font-bold">Creating database entries</h2>
{#if supabase_status === StepStatus.Waiting}
<span class="text-black-600">Waiting...</span>
{:else if supabase_status === StepStatus.Loading}
<span class="text-black-600">Creating entries...</span>
{:else if supabase_status === StepStatus.Success}
<span class="text-green-600">Database entries created successfully.</span>
{:else if supabase_status === StepStatus.Failure}
<span class="text-red-600">Failed to create database entries.</span>
{/if}
</div>
<!-- Sending Emails -->
<div class="rounded border border-gray-300 bg-white p-4">
<h2 class="mb-2 text-xl font-bold">Sending emails</h2>
{#if email_status === StepStatus.Waiting}
<span class="text-black-600">Waiting...</span>
{:else if email_status === StepStatus.Loading}
<span class="text-black-600">Sending emails...</span>
{:else if email_status === StepStatus.Success}
<span class="text-green-600">Emails sent successfully.</span>
{:else if email_status === StepStatus.Failure}
<span class="text-red-600">Failed to send emails.</span>
{/if}
</div>

View File

@@ -2,10 +2,8 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
export let authorized = false;
let refreshToken = '';
let loading = true;
let authorized = false;
let to = '';
let subject = '';
@@ -24,15 +22,30 @@
}
onMount(async () => {
console.log("on mount");
refreshToken = localStorage.getItem('gmail_refresh_token') ?? '';
loading = true;
authorized = await validateToken(refreshToken);
loading = false;
});
/* ⇢ redirects straight to Google via server 302 */
const connect = () => goto('/private/api/gmail?action=auth');
async function sendEmail() {
const r = await fetch('/private/api/gmail', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'send',
to,
subject,
text: body,
refreshToken
})
});
r.ok ? alert('Sent!') : alert(await r.text());
to = subject = body = '';
}
async function disconnect() {
if (!confirm('Disconnect Google account?')) return;
await fetch('/private/api/gmail', {
@@ -46,30 +59,11 @@
}
</script>
<div class="mb-4 rounded border border-gray-300 bg-white p-4">
{#if loading}
<div class="flex items-center space-x-2">
<svg class="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
<span>Checking Google connection...</span>
</div>
{:else}
{#if !authorized}
<section class="flex items-center justify-between w-full">
<p class="mr-4">You havent connected your Google account yet.</p>
<button class="btn bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded ml-auto" on:click={connect}>
Connect Google
</button>
{#if !authorized}
<section class="space-y-4">
<p>You havent connected your Google account yet.</p>
<button class="btn" on:click={connect}>Connect Google</button>
</section>
{:else}
<div class="flex items-center space-x-2 text-green-600">
<svg class="h-5 w-5" 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>Your connection to Google is good, proceed to next step</span>
</div>
{/if}
{/if}
</div>
{:else}
Your connection is good, proceed to next step
{/if}

View File

@@ -1,5 +1,6 @@
<script lang="ts">
export let email: { subject: string, body: string } = { subject: '', body: '' };
export let subject = '';
export let body = '';
</script>
<form class="flex flex-col space-y-4 bg-white p-8 rounded border border-gray-300 w-full shadow-none">
@@ -8,7 +9,7 @@
Subject
<input
type="text"
bind:value={email.subject}
bind:value={subject}
class="mt-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-200"
required
/>
@@ -16,7 +17,7 @@
<label class="flex flex-col text-gray-700">
Body
<textarea
bind:value={email.body}
bind:value={body}
class="mt-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-200 resize-none"
rows="6"
required

View File

@@ -1,21 +1,12 @@
<script lang="ts">
import { enhance } from '$app/forms';
let { event } = $props();
let loading = $state(false);
let { events, new_event } = $props();
function handleEnhance() {
loading = true;
return async ({ update }) => {
await update();
loading = false;
};
}
</script>
<form method="POST" action="?/create" use:enhance={handleEnhance} class="flex flex-col space-y-4 bg-white p-8 rounded border border-gray-300 w-full shadow-none">
<form method="POST" action="?/create" use:enhance class="flex flex-col space-y-4 bg-white p-8 rounded border border-gray-300 w-full shadow-none">
<h2 class="text-2xl font-semibold text-center mb-4">Create Event</h2>
<label class="flex flex-col text-gray-700">
Name
@@ -52,20 +43,12 @@
</button>
</form>
{#if Object.keys(event).length === 0}
<div class="mt-4 rounded border-l-4 border-gray-500 bg-gray-100 p-4 text-gray-700">
{#if loading}
<strong>Loading...</strong>
{:else}
<strong>No event created yet...</strong>
{/if}
</div>
{:else}
{#if Object.keys(new_event).length > 0}
<div class="rounded border-l-4 border-green-500 bg-green-100 p-4 text-green-700 mt-4">
<ol>
<li><strong>{event.name}</strong></li>
<li>{event.date}</li>
<li>{event.description}</li>
<li><strong>{new_event.name}</strong></li>
<li>{new_event.date}</li>
<li>{new_event.description}</li>
</ol>
</div>
{/if}

View File

@@ -1,59 +0,0 @@
<script lang="ts">
import QRCode from 'qrcode';
const StepState = {
Waiting: 'waiting',
Processing: 'processing',
FinishedSuccess: 'finished_success',
FinishedFail: 'finished_fail'
};
let qr_codes_state = $state(StepState.Processing);
let emails_state = $state(StepState.FinishedSuccess);
// Inserts all participants into the database and returns their assigned IDs.
async function insert_data_supabase(data, participants, new_event) {
const names = participants.map((p) => p.name);
const surnames = participants.map((p) => p.surname);
const emails = participants.map((p) => p.email);
const {
data: { user },
error: authError
} = await data.supabase.auth.getUser();
const { data: user_profile, error: profileError } = await data.supabase
.from('profiles')
.select('*, section:sections (id, name)')
.eq('id', user?.id)
.single();
const { data: result, error: qrCodeError } = await data.supabase.rpc('create_qrcodes_bulk', {
p_section_id: user_profile?.section.id,
p_event_id: new_event.id,
p_names: names,
p_surnames: surnames,
p_emails: emails
});
return { result };
}
// Creates a base64 interpretation of the ticket ID
function createB64QRCode(data) {
QRCode.toDataURL('I am a pony!')
.then((url) => {
const parts = url.split(',');
return { base64data: parts[1] };
})
.catch((err) => {
console.error(err);
});
}
function sendEmail(email, subject, body, qr_code_base64) {
// Here you would implement the logic to send the email.
// This is a placeholder function.
console.log(`Sending email to ${email} with subject "${subject}" and body "${body}"`);
console.log(`QR Code Base64: ${qr_code_base64}`);
}
</script>
Pl

View File

@@ -1,78 +1,35 @@
<script lang="ts">
import { goto } from '$app/navigation';
let { data, event, participants, email, stepConditions } = $props();
function redirectToFinish() {
// Generate a random variable name
const varName = 'event_' + Math.random().toString(36).substr(2, 9);
// Save the data to sessionStorage
sessionStorage.setItem(
varName,
JSON.stringify({ event, participants, email })
);
// Redirect with the variable name as a query parameter
goto(`/private/creator/finish?data=${encodeURIComponent(varName)}`);
}
let { new_event, participants, subject, body } = $props();
</script>
<!-- New Event Overview -->
<div class="mb-4 rounded border border-gray-300 bg-white p-4">
<h2 class="mb-2 text-xl font-bold">Event Overview</h2>
<div class="bg-white border border-gray-300 rounded p-6 mb-6">
<h2 class="text-xl font-bold mb-2">Event Overview</h2>
<ul class="space-y-1">
<li><span class="font-semibold">Name:</span> {event.name}</li>
<li><span class="font-semibold">Date:</span> {event.date}</li>
<li><span class="font-semibold">Description:</span> {event.description}</li>
<li><span class="font-semibold">Name:</span> {new_event.name}</li>
<li><span class="font-semibold">Date:</span> {new_event.date}</li>
<li><span class="font-semibold">Description:</span> {new_event.description}</li>
</ul>
</div>
<!-- Email Overview -->
<div class="mb-4 rounded border border-gray-300 bg-white p-4">
<h2 class="mb-2 text-xl font-bold">Email Preview</h2>
<div class="mb-2"><span class="font-semibold">Subject:</span> {email.subject}</div>
<div class="rounded border bg-gray-50 p-2 whitespace-pre-line text-gray-700">
<span class="font-semibold"></span>
<div>{email.body}</div>
<div class="bg-white border border-gray-300 rounded p-6 mb-6">
<h2 class="text-xl font-bold mb-2">Email Preview</h2>
<div class="mb-2"><span class="font-semibold">Subject:</span> {subject}</div>
<div class="whitespace-pre-line border rounded p-2 bg-gray-50 text-gray-700"><span class="font-semibold">Body:</span>
<div class="mt-1">{body}</div>
</div>
</div>
<!-- Participants Overview -->
<div class="rounded border border-gray-300 bg-white p-4">
<h2 class="mb-2 text-xl font-bold">Participants ({participants.length})</h2>
<div class="bg-white border border-gray-300 rounded p-6">
<h2 class="text-xl font-bold mb-2">Participants ({participants.length})</h2>
<ul class="space-y-1">
{#each participants.slice(0, 10) as p}
<li class="flex items-center gap-2 border-b pb-1 last:border-b-0">
{#each participants as p}
<li class="flex items-center gap-2 border-b last:border-b-0 pb-1">
<span class="font-semibold">{p.name} {p.surname}</span>
<span class="flex-1"></span>
<span class="text-right font-mono text-xs text-gray-600">{p.email}</span>
<span class="font-mono text-xs text-gray-600">{p.email}</span>
</li>
{/each}
</ul>
<p class="mt-2 text-sm text-gray-500">Note: Only the first 10 participants are shown.</p>
</div>
<button
onclick={redirectToFinish}
class="mt-4 w-full rounded bg-blue-600 px-4 py-3 font-bold text-white
transition-colors duration-200 hover:bg-blue-700
disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-500"
disabled={!stepConditions.every(Boolean)}
>
Generate QR codes and send
</button>
<div class="mt-2 space-y-1">
{#if !stepConditions[0]}
<p class="text-sm text-red-500">Please provide an event name before proceeding.</p>
{/if}
{#if !stepConditions[1]}
<p class="text-sm text-red-500">Please add at least one participant before proceeding.</p>
{/if}
{#if !stepConditions[2]}
<p class="text-sm text-red-500">Please provide an email subject before proceeding.</p>
{/if}
{#if !stepConditions[3]}
<p class="text-sm text-red-500">Please provide an email body before proceeding.</p>
{/if}
</div>

View File

@@ -2,26 +2,11 @@
import { enhance } from '$app/forms';
let { participants = [] } = $props();
let loading = $state(false);
function handleEnhance() {
loading = true;
return async ({ update }) => {
await update();
loading = false;
};
}
</script>
<form
method="POST"
action="?/participants"
use:enhance={handleEnhance}
enctype="multipart/form-data"
class="flex w-full flex-col space-y-4 rounded border border-gray-300 bg-white p-8 shadow-none"
>
<h2 class="mb-4 text-center text-2xl font-semibold">Upload Participants</h2>
<form method="POST" action="?/participants" use:enhance enctype="multipart/form-data" class="flex flex-col space-y-4 bg-white p-8 rounded border border-gray-300 w-full shadow-none">
<h2 class="text-2xl font-semibold text-center mb-4">Upload Participants</h2>
<label class="flex flex-col text-gray-700">
CSV File
<input
@@ -29,34 +14,30 @@
name="participants"
id="participants"
accept=".csv"
class="mt-1 rounded border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-blue-200 focus:outline-none"
class="mt-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-200"
required
/>
</label>
<button
type="submit"
class="w-full rounded bg-blue-600 py-2 text-white transition hover:bg-blue-700"
class="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition"
>
Submit
</button>
</form>
{#if participants.length === 0}
<div class="mt-4 rounded border-l-4 border-gray-500 bg-gray-100 p-4 text-gray-700">
{#if loading}
<strong>Loading...</strong>
{:else}
<strong>No participants yet...</strong>
{/if}
</div>
{:else}
<div class="mt-4 rounded border-l-4 border-green-500 bg-green-50 p-4 text-green-700">
<p class="text-gray-500 mt-4">No participants added yet.</p>
{/if}
{#if participants.length > 0}
<div class="rounded border-l-4 border-green-500 bg-green-50 p-4 text-green-700 mt-4">
<ul class="space-y-2">
{#each participants as p, i}
<li class="flex items-center justify-between border-b pb-1">
<div>
<div class="font-semibold">{p.name} {p.surname}</div>
<div class="font-mono text-xs text-gray-600">{p.email}</div>
<div class="text-xs font-mono text-gray-600">{p.email}</div>
</div>
</li>
{/each}

View File

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

View File

@@ -1,25 +0,0 @@
<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

@@ -1,13 +0,0 @@
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

@@ -1,95 +0,0 @@
<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,45 +7,13 @@
};
</script>
<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 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>
</div>
<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>
<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>

View File

@@ -12,10 +12,11 @@
$effect(() => {
if (scanned_id === "") return;
console.log('New QR code found:', scanned_id);
scan_state = ScanState.scanning;
data.supabase
.from('participants')
.from('qrcodes')
.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, onDestroy } from 'svelte';
import { onMount } from 'svelte';
import {
Html5QrcodeScanner,
type Html5QrcodeResult,
@@ -37,12 +37,6 @@
);
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,8 +1,7 @@
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(), devtoolsJson()]
plugins: [tailwindcss(), sveltekit()]
});