Compare commits

..

6 Commits

Author SHA1 Message Date
e2d7de1e9e chore(release): 1.9.10
All checks were successful
Build release images / build-container (push) Successful in 1m0s
2025-04-08 21:57:18 +02:00
d7c9c27ec7 feat: add experimental ui for mobile card assignment 2025-04-08 21:56:23 +02:00
153b1b3c2b chore(release): 1.9.9
All checks were successful
Build release images / build-container (push) Successful in 55s
2025-04-04 22:40:24 +02:00
ec63c7c1c5 fix(CopyScanStationTokenModal): code sizes 2025-04-04 22:40:04 +02:00
05c2535698 chore(release): 1.9.8
All checks were successful
Build release images / build-container (push) Successful in 57s
2025-04-02 22:04:18 +02:00
e261d5e345 feat(GenerateSponsoringContracts): show download progress
commit c3226c37c9
Author: Philipp Dormann <philipp@philippdormann.de>
Date:   Wed Apr 2 22:03:06 2025 +0200

    wip

commit 210140fd67
Author: Philipp Dormann <philipp@philippdormann.de>
Date:   Wed Apr 2 21:58:07 2025 +0200

    wip

commit 35e58d233e
Author: Philipp Dormann <philipp@philippdormann.de>
Date:   Wed Apr 2 21:55:21 2025 +0200

    wip

commit a09bf31e22
Author: Philipp Dormann <philipp@philippdormann.de>
Date:   Wed Apr 2 21:48:51 2025 +0200

    wip

commit 7c31fba83a
Author: Philipp Dormann <philipp@philippdormann.de>
Date:   Wed Apr 2 21:48:37 2025 +0200

    wip
2025-04-02 22:03:47 +02:00
12 changed files with 320 additions and 14 deletions

View File

@@ -2,9 +2,30 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.9.10](https://git.odit.services/lfk/frontend/compare/1.9.9...1.9.10)
- feat: add experimental ui for mobile card assignment [`d7c9c27`](https://git.odit.services/lfk/frontend/commit/d7c9c27ec7a1fea1cbaf26914843d044bbae32fe)
#### [1.9.9](https://git.odit.services/lfk/frontend/compare/1.9.8...1.9.9)
> 4 April 2025
- chore(release): 1.9.9 [`153b1b3`](https://git.odit.services/lfk/frontend/commit/153b1b3c2badee4826be614c3dbaafc10e1fbfea)
- fix(CopyScanStationTokenModal): code sizes [`ec63c7c`](https://git.odit.services/lfk/frontend/commit/ec63c7c1c51ccaf25bdd1eacffda66c820003a4c)
#### [1.9.8](https://git.odit.services/lfk/frontend/compare/1.9.7...1.9.8)
> 2 April 2025
- feat(GenerateSponsoringContracts): show download progress [`e261d5e`](https://git.odit.services/lfk/frontend/commit/e261d5e345f3175672bf86646ed838dd23400e50)
- chore(release): 1.9.8 [`05c2535`](https://git.odit.services/lfk/frontend/commit/05c253569877a45f3c4759262255ca70aa9ee4a3)
#### [1.9.7](https://git.odit.services/lfk/frontend/compare/1.9.6...1.9.7)
> 2 April 2025
- fix: ImportRunnerModal scrolling & team select [`766eeab`](https://git.odit.services/lfk/frontend/commit/766eeab49fb3ca5715c19dbf9bc53cb71124d3df)
- chore(release): 1.9.7 [`c00497d`](https://git.odit.services/lfk/frontend/commit/c00497d7760a935965cc83213f72f35999a3c168)
#### [1.9.6](https://git.odit.services/lfk/frontend/compare/1.9.5...1.9.6)

View File

@@ -13,7 +13,7 @@
<body>
<span style="display: none; visibility: hidden" id="buildinfo"
>RELEASE_INFO-1.9.7-RELEASE_INFO</span
>RELEASE_INFO-1.9.10-RELEASE_INFO</span
>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script src="/env.js"></script>

View File

@@ -1,6 +1,6 @@
{
"name": "@odit/lfk-frontend",
"version": "1.9.7",
"version": "1.9.10",
"type": "module",
"scripts": {
"i18n-order": "node order.js",
@@ -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
View File

@@ -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

View File

@@ -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 />

View 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>

View 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>

View File

@@ -0,0 +1,81 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { onMount } from "svelte";
export let download_details = "";
export let modal_open;
onMount(() => {
document.onkeydown = (e) => {
e = e || window.event;
if (e.key === "Escape") {
modal_open = false;
}
if (e.keyCode === 13) {
if (createbtnenabled === true) {
createbtnenabled = false;
submit();
}
}
};
});
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-hidden"
use:clickOutside
on:click_outside={() => {
modal_open = false;
}}
>
<div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw]"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl">
<div class="">
<div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-6 w-6 text-blue-600"
fill="currentColor"
width="24"
height="24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M9.83 8.79L8 9.456V13H6V8.05h.015l5.268-1.918c.244-.093.51-.14.782-.131a2.616 2.616 0 0 1 2.427 1.82c.186.583.356.977.51 1.182A4.992 4.992 0 0 0 19 11v2a6.986 6.986 0 0 1-5.402-2.547l-.581 3.297L15 15.67V23h-2v-5.986l-2.05-1.987-.947 4.298-6.894-1.215.348-1.97 4.924.868L9.83 8.79zM13.5 5.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"
/></svg
>
</div>
<div class="mt-3 sm:text-left text-base">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_('download_laeuft')}
</h3>
<div class="w-full">
{download_details}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/if}

View File

@@ -7,6 +7,7 @@
import DocumentServer from "./DocumentServer";
import { init } from "@paralleldrive/cuid2";
import toast from "svelte-french-toast";
import DownloadProgressModal from "./DownloadProgressModal.svelte";
const createId = init({ length: 10, fingerprint: "lfk-frontend" });
const documentServer = new DocumentServer(
config.baseurl_documentserver,
@@ -17,8 +18,12 @@
export let generate_runners = [];
export let generate_orgs = [];
export let generate_teams = [];
//
export let download_modal_open = false;
export let download_details = "";
function generateSponsoringContract(locale) {
download_modal_open = true;
if (generate_orgs.length > 0) {
generateOrgContracts(locale);
} else if (generate_teams.length > 0) {
@@ -41,19 +46,24 @@
async function generateTeamContracts(locale) {
toast.loading($_("generating-pdfs"));
let totalCount = generate_teams.length;
let count = 0;
for (const t of generate_teams) {
count++;
download_details = `${t.parentGroup.name} > ${t.name}`;
const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id
);
documentServer
await documentServer
.generateContracts(runners, locale)
.then((blob) => {
download(
blob,
`${$_("sponsorings")}_${t.name}-${locale}-${createId()}.pdf`
);
if (count === totalCount) {
download_modal_open = false;
}
})
.catch((err) => {});
}
@@ -61,15 +71,23 @@
async function generateOrgContracts(locale) {
toast.loading($_("generating-pdf"));
let count_orgs = 0;
let totalCount = 0;
for (const o of generate_orgs) {
count_orgs++;
let count = 0;
totalCount++;
for (const t of o.teams) {
totalCount++;
}
}
console.log({ totalCount });
let count = 0;
for (const o of generate_orgs) {
count++;
let runners =
await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
o.id,
true
);
download_details = o.name;
await documentServer
.generateContracts(runners, locale)
.then((blob) => {
@@ -84,6 +102,7 @@
let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id
);
download_details = `${o.name} > ${t.name}`;
await documentServer
.generateContracts(runners, locale)
.then((blob) => {
@@ -93,15 +112,19 @@
t.name
}-${locale}-${createId()}.pdf`
);
console.log({ count });
if (count === totalCount) {
download_modal_open = false;
}
})
.catch((err) => {});
}
}
}
function generateRunnerContracts(locale) {
async function generateRunnerContracts(locale) {
toast.loading($_("generating-pdf"));
documentServer
await documentServer
.generateContracts(generate_runners, locale)
.then((blob) => {
let fileName = `${$_("sponsorings")}-${locale}-${createId()}.pdf`;
@@ -111,6 +134,7 @@
}-${locale}-${createId()}.pdf`;
}
download(blob, fileName);
download_modal_open = false;
})
.catch((err) => {
console.error(err);
@@ -119,6 +143,7 @@
</script>
{#if sponsoring_contracts_show}
<DownloadProgressModal {download_details} modal_open={download_modal_open} />
<button
on:click={() => {
generateSponsoringContract("de");

View File

@@ -170,7 +170,7 @@
<img
class:w-[50%]={is_qrcode}
class:w-full={!is_qrcode}
class="md:w-auto mb-2 mx-auto"
class="w-full lg:max-w-[50vw] lg:max-h-[10rem] object-contain mb-2 mx-auto"
alt="Registrierungscode"
src={textToBase64Barcode(window.config.baseurl, is_qrcode)}
/>
@@ -178,7 +178,7 @@
<img
class:w-[50%]={is_qrcode}
class:w-full={!is_qrcode}
class="md:w-auto mb-2 mx-auto"
class="w-full lg:max-w-[50vw] lg:max-h-[10rem] object-contain mb-2 mx-auto"
alt="Registrierungscode"
src={barcode}
/>

View File

@@ -204,6 +204,7 @@
"donors-are-being-loaded": "Sponsoren werden geladen",
"dont-have-your-email-connected": "Deine E-Mail ist nicht verknüpft?",
"dont-panic-were-resetting-it": "Keine Panik, wir setzen es zurück ✌",
"download_laeuft": "Download läuft...",
"e-mail-adress": "E-Mail-Adresse",
"edit": "Bearbeiten",
"edit-a-card": "Läuferkarte bearbeiten",

View File

@@ -204,6 +204,7 @@
"donors-are-being-loaded": "donors are being loaded",
"dont-have-your-email-connected": "Don't have your email connected?",
"dont-panic-were-resetting-it": "Don't panic, we're resetting it ✌",
"download_laeuft": "Download in progress...",
"e-mail-adress": "E-Mail Adress",
"edit": "Edit",
"edit-a-card": "Edit a card",