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",
|
"bwip-js": "3.4.0",
|
||||||
"check-password-strength": "2.0.10",
|
"check-password-strength": "2.0.10",
|
||||||
"csvtojson": "2.0.10",
|
"csvtojson": "2.0.10",
|
||||||
|
"html5-qrcode": "^2.3.8",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
"marked": "4.3.0",
|
"marked": "4.3.0",
|
||||||
"svelte": "3.58.0",
|
"svelte": "3.58.0",
|
||||||
|
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@ -29,6 +29,9 @@ importers:
|
|||||||
csvtojson:
|
csvtojson:
|
||||||
specifier: 2.0.10
|
specifier: 2.0.10
|
||||||
version: 2.0.10
|
version: 2.0.10
|
||||||
|
html5-qrcode:
|
||||||
|
specifier: ^2.3.8
|
||||||
|
version: 2.3.8
|
||||||
localforage:
|
localforage:
|
||||||
specifier: 1.10.0
|
specifier: 1.10.0
|
||||||
version: 1.10.0
|
version: 1.10.0
|
||||||
@ -77,7 +80,7 @@ importers:
|
|||||||
version: 3.3.3(prettier@3.5.3)(svelte@3.58.0)
|
version: 3.3.3(prettier@3.5.3)(svelte@3.58.0)
|
||||||
release-it:
|
release-it:
|
||||||
specifier: 17.10.0
|
specifier: 17.10.0
|
||||||
version: 17.10.0
|
version: 17.10.0(typescript@5.8.3)
|
||||||
svelte-select:
|
svelte-select:
|
||||||
specifier: 3.17.0
|
specifier: 3.17.0
|
||||||
version: 3.17.0
|
version: 3.17.0
|
||||||
@ -924,6 +927,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
html5-qrcode@2.3.8:
|
||||||
|
resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==}
|
||||||
|
|
||||||
http-proxy-agent@7.0.2:
|
http-proxy-agent@7.0.2:
|
||||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@ -1794,6 +1800,11 @@ packages:
|
|||||||
type@2.7.2:
|
type@2.7.2:
|
||||||
resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==}
|
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:
|
uglify-js@3.19.3:
|
||||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||||
engines: {node: '>=0.8.0'}
|
engines: {node: '>=0.8.0'}
|
||||||
@ -2439,12 +2450,14 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
xdg-basedir: 5.1.0
|
xdg-basedir: 5.1.0
|
||||||
|
|
||||||
cosmiconfig@9.0.0:
|
cosmiconfig@9.0.0(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
env-paths: 2.2.1
|
env-paths: 2.2.1
|
||||||
import-fresh: 3.3.0
|
import-fresh: 3.3.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
parse-json: 5.2.0
|
parse-json: 5.2.0
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.8.3
|
||||||
|
|
||||||
crc-32@1.2.2: {}
|
crc-32@1.2.2: {}
|
||||||
|
|
||||||
@ -2765,6 +2778,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
html5-qrcode@2.3.8: {}
|
||||||
|
|
||||||
http-proxy-agent@7.0.2:
|
http-proxy-agent@7.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.1
|
agent-base: 7.1.1
|
||||||
@ -3322,14 +3337,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
rc: 1.2.8
|
rc: 1.2.8
|
||||||
|
|
||||||
release-it@17.10.0:
|
release-it@17.10.0(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iarna/toml': 2.2.5
|
'@iarna/toml': 2.2.5
|
||||||
'@octokit/rest': 20.1.1
|
'@octokit/rest': 20.1.1
|
||||||
async-retry: 1.3.3
|
async-retry: 1.3.3
|
||||||
chalk: 5.3.0
|
chalk: 5.3.0
|
||||||
ci-info: 4.1.0
|
ci-info: 4.1.0
|
||||||
cosmiconfig: 9.0.0
|
cosmiconfig: 9.0.0(typescript@5.8.3)
|
||||||
execa: 8.0.0
|
execa: 8.0.0
|
||||||
git-url-parse: 14.0.0
|
git-url-parse: 14.0.0
|
||||||
globby: 14.0.2
|
globby: 14.0.2
|
||||||
@ -3607,6 +3622,9 @@ snapshots:
|
|||||||
|
|
||||||
type@2.7.2: {}
|
type@2.7.2: {}
|
||||||
|
|
||||||
|
typescript@5.8.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
uglify-js@3.19.3:
|
uglify-js@3.19.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
import Settings from "./components/settings/Settings.svelte";
|
import Settings from "./components/settings/Settings.svelte";
|
||||||
import Transition from "./components/base/Transition.svelte";
|
import Transition from "./components/base/Transition.svelte";
|
||||||
import Orgs from "./components/orgs/Orgs.svelte";
|
import Orgs from "./components/orgs/Orgs.svelte";
|
||||||
|
import CardAssignment from "./components/general/CardAssignment.svelte";
|
||||||
import Runners from "./components/runners/Runners.svelte";
|
import Runners from "./components/runners/Runners.svelte";
|
||||||
import Footer from "./components/general/Footer.svelte";
|
import Footer from "./components/general/Footer.svelte";
|
||||||
import TracksOverview from "./components/tracks/TracksOverview.svelte";
|
import TracksOverview from "./components/tracks/TracksOverview.svelte";
|
||||||
@ -141,6 +142,11 @@
|
|||||||
<RunnerDetail {params} />
|
<RunnerDetail {params} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/cardassignment/*">
|
||||||
|
<Route path="/">
|
||||||
|
<CardAssignment />
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
<Route path="/teams/*">
|
<Route path="/teams/*">
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Teams />
|
<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