feat: add experimental ui for mobile card assignment
This commit is contained in:
parent
153b1b3c2b
commit
d7c9c27ec7
@ -49,6 +49,7 @@
|
||||
"bwip-js": "3.4.0",
|
||||
"check-password-strength": "2.0.10",
|
||||
"csvtojson": "2.0.10",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"localforage": "1.10.0",
|
||||
"marked": "4.3.0",
|
||||
"svelte": "3.58.0",
|
||||
|
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@ -29,6 +29,9 @@ importers:
|
||||
csvtojson:
|
||||
specifier: 2.0.10
|
||||
version: 2.0.10
|
||||
html5-qrcode:
|
||||
specifier: ^2.3.8
|
||||
version: 2.3.8
|
||||
localforage:
|
||||
specifier: 1.10.0
|
||||
version: 1.10.0
|
||||
@ -77,7 +80,7 @@ importers:
|
||||
version: 3.3.3(prettier@3.5.3)(svelte@3.58.0)
|
||||
release-it:
|
||||
specifier: 17.10.0
|
||||
version: 17.10.0
|
||||
version: 17.10.0(typescript@5.8.3)
|
||||
svelte-select:
|
||||
specifier: 3.17.0
|
||||
version: 3.17.0
|
||||
@ -924,6 +927,9 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
html5-qrcode@2.3.8:
|
||||
resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
@ -1794,6 +1800,11 @@ packages:
|
||||
type@2.7.2:
|
||||
resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==}
|
||||
|
||||
typescript@5.8.3:
|
||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@ -2439,12 +2450,14 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
xdg-basedir: 5.1.0
|
||||
|
||||
cosmiconfig@9.0.0:
|
||||
cosmiconfig@9.0.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
import-fresh: 3.3.0
|
||||
js-yaml: 4.1.0
|
||||
parse-json: 5.2.0
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
crc-32@1.2.2: {}
|
||||
|
||||
@ -2765,6 +2778,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
html5-qrcode@2.3.8: {}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
@ -3322,14 +3337,14 @@ snapshots:
|
||||
dependencies:
|
||||
rc: 1.2.8
|
||||
|
||||
release-it@17.10.0:
|
||||
release-it@17.10.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@iarna/toml': 2.2.5
|
||||
'@octokit/rest': 20.1.1
|
||||
async-retry: 1.3.3
|
||||
chalk: 5.3.0
|
||||
ci-info: 4.1.0
|
||||
cosmiconfig: 9.0.0
|
||||
cosmiconfig: 9.0.0(typescript@5.8.3)
|
||||
execa: 8.0.0
|
||||
git-url-parse: 14.0.0
|
||||
globby: 14.0.2
|
||||
@ -3607,6 +3622,9 @@ snapshots:
|
||||
|
||||
type@2.7.2: {}
|
||||
|
||||
typescript@5.8.3:
|
||||
optional: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
optional: true
|
||||
|
||||
|
@ -41,6 +41,7 @@
|
||||
import Settings from "./components/settings/Settings.svelte";
|
||||
import Transition from "./components/base/Transition.svelte";
|
||||
import Orgs from "./components/orgs/Orgs.svelte";
|
||||
import CardAssignment from "./components/general/CardAssignment.svelte";
|
||||
import Runners from "./components/runners/Runners.svelte";
|
||||
import Footer from "./components/general/Footer.svelte";
|
||||
import TracksOverview from "./components/tracks/TracksOverview.svelte";
|
||||
@ -141,6 +142,11 @@
|
||||
<RunnerDetail {params} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="/cardassignment/*">
|
||||
<Route path="/">
|
||||
<CardAssignment />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="/teams/*">
|
||||
<Route path="/">
|
||||
<Teams />
|
||||
|
72
src/components/general/CardAssignment.svelte
Normal file
72
src/components/general/CardAssignment.svelte
Normal file
@ -0,0 +1,72 @@
|
||||
<script>
|
||||
import QrCodeScanner from "./QrCodeScanner.svelte";
|
||||
let state = "scan_runner";
|
||||
let runnerID = undefined;
|
||||
let cardInfo = "";
|
||||
</script>
|
||||
|
||||
<div class="p-4">
|
||||
<h3 class="text-3xl font-bold">Card Assignment for Mobile</h3>
|
||||
{#if state === "done"}
|
||||
<p>Assigned Card {cardInfo} ✅</p>
|
||||
<p>(not really, needs to be implemented)</p>
|
||||
{:else}
|
||||
<!-- -->
|
||||
{#if state === "scan_runner"}
|
||||
<h3 class="text-xl font-bold">Scan Runner (Selfservice QR)</h3>
|
||||
{/if}
|
||||
{#if state === "scan_card"}
|
||||
<h3 class="text-xl font-bold">Runner Scanned</h3>
|
||||
<p>{runnerID}</p>
|
||||
<h3 class="text-xl font-bold">Scan Card (Code 128 Barcode)</h3>
|
||||
{/if}
|
||||
<QrCodeScanner
|
||||
on:detect={(e) => {
|
||||
console.log({ type: "DETECT", code: e.detail.decodedText });
|
||||
if (state === "scan_runner") {
|
||||
if (
|
||||
e.detail.decodedText.includes(
|
||||
"https://portal.lauf-fuer-kaya.de/profile/"
|
||||
)
|
||||
) {
|
||||
runnerID = JSON.parse(
|
||||
atob(
|
||||
e.detail.decodedText
|
||||
.replace("https://portal.lauf-fuer-kaya.de/profile/", "")
|
||||
.split(".")[1]
|
||||
)
|
||||
).id;
|
||||
state = "scan_card";
|
||||
}
|
||||
}
|
||||
if (state === "scan_card") {
|
||||
if (
|
||||
!e.detail.decodedText.includes(
|
||||
"https://portal.lauf-fuer-kaya.de/profile/"
|
||||
)
|
||||
) {
|
||||
cardInfo = e.detail.decodedText;
|
||||
state = "done";
|
||||
}
|
||||
}
|
||||
}}
|
||||
width={320}
|
||||
height={320}
|
||||
class="w-full max-w-sm bg-neutral-300 rounded-lg overflow-hidden"
|
||||
/>
|
||||
{#if state === "scan_card"}
|
||||
<button
|
||||
on:click={() => {
|
||||
state = "scan_runner";
|
||||
runnerID = undefined;
|
||||
cardInfo = "";
|
||||
}}
|
||||
type="button"
|
||||
class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-red-100 text-red-800 hover:bg-red-200 focus:outline-hidden focus:bg-red-200 disabled:opacity-50 disabled:pointer-events-none dark:text-red-500 dark:bg-red-800/30 dark:hover:bg-red-800/20 dark:focus:bg-red-800/20 w-full mt-2"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{/if}
|
||||
<!-- -->
|
||||
{/if}
|
||||
</div>
|
80
src/components/general/QrCodeScanner.svelte
Normal file
80
src/components/general/QrCodeScanner.svelte
Normal file
@ -0,0 +1,80 @@
|
||||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import {
|
||||
Html5QrcodeScanner,
|
||||
Html5QrcodeScanType,
|
||||
Html5QrcodeSupportedFormats,
|
||||
Html5QrcodeScannerState,
|
||||
} from "html5-qrcode";
|
||||
|
||||
export let width;
|
||||
export let height;
|
||||
export let paused = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
dispatch("detect", { decodedText });
|
||||
}
|
||||
|
||||
// usually better to ignore and keep scanning
|
||||
function onScanFailure(message) {
|
||||
dispatch("error", { message });
|
||||
}
|
||||
|
||||
let scanner;
|
||||
onMount(() => {
|
||||
scanner = new Html5QrcodeScanner(
|
||||
"qr-scanner",
|
||||
{
|
||||
fps: 10,
|
||||
rememberLastUsedCamera: true,
|
||||
qrbox: { width, height },
|
||||
aspectRatio: 1,
|
||||
supportedScanTypes: [Html5QrcodeScanType.SCAN_TYPE_CAMERA],
|
||||
formatsToSupport: [
|
||||
Html5QrcodeSupportedFormats.CODE_39,
|
||||
Html5QrcodeSupportedFormats.EAN_8,
|
||||
Html5QrcodeSupportedFormats.EAN_13,
|
||||
Html5QrcodeSupportedFormats.QR_CODE,
|
||||
Html5QrcodeSupportedFormats.CODE_128,
|
||||
],
|
||||
},
|
||||
false // non-verbose
|
||||
);
|
||||
scanner.render(onScanSuccess, onScanFailure);
|
||||
});
|
||||
|
||||
// pause/resume scanner to avoid unintended scans
|
||||
$: togglePause(paused);
|
||||
function togglePause(paused) {
|
||||
if (paused && scanner?.getState() === Html5QrcodeScannerState.SCANNING) {
|
||||
scanner?.pause();
|
||||
} else if (scanner?.getState() === Html5QrcodeScannerState.PAUSED) {
|
||||
scanner?.resume();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="qr-scanner" class={$$props.class} />
|
||||
|
||||
<style>
|
||||
/* Hide unwanted icons */
|
||||
#qr-scanner :global(img[alt="Info icon"]),
|
||||
#qr-scanner :global(img[alt="Camera based scan"]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Change camera permission button text */
|
||||
#qr-scanner :global(#html5-qrcode-button-camera-permission) {
|
||||
visibility: hidden;
|
||||
}
|
||||
#qr-scanner :global(#html5-qrcode-button-camera-permission::after) {
|
||||
position: absolute;
|
||||
inset: auto 0 0;
|
||||
display: block;
|
||||
content: "Allow camera access";
|
||||
visibility: visible;
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user