Compare commits

..

No commits in common. "1.14.0" and "1.13.5" have entirely different histories.

7 changed files with 370 additions and 778 deletions

View File

@ -2,32 +2,10 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.14.0](https://git.odit.services/lfk/frontend/compare/1.13.5...1.14.0)
- wip [`564a971`](https://git.odit.services/lfk/frontend/commit/564a971c63403af2e2eb550db814519576d62023)
- wip [`50b5e4e`](https://git.odit.services/lfk/frontend/commit/50b5e4e455ce705fc5ef7f3d069d88c9ff48a6af)
- wip [`2c91f46`](https://git.odit.services/lfk/frontend/commit/2c91f463758c8452561fbcc5dad8412edba8915d)
- wip [`1386b80`](https://git.odit.services/lfk/frontend/commit/1386b80d0c8569cf127f8235b3dd249c2775594a)
- wip [`6ef6dc0`](https://git.odit.services/lfk/frontend/commit/6ef6dc007837c237273a29ca489ef0cdb92f7c6c)
- wip [`3709881`](https://git.odit.services/lfk/frontend/commit/370988117683ab1fdc149a30f920cc6a66575c7a)
- wip [`77413c7`](https://git.odit.services/lfk/frontend/commit/77413c7e5350a1d8643d2baf135b531235f78e64)
- wip [`0cb1193`](https://git.odit.services/lfk/frontend/commit/0cb1193269912b047abfacb6012463093c2adcfa)
- wip [`9ef3435`](https://git.odit.services/lfk/frontend/commit/9ef34359d8ac32674c28825b91b6aa2877e63552)
- wip [`a00af08`](https://git.odit.services/lfk/frontend/commit/a00af08b3f7c8278cfc54af6f593a9dcf4509ab4)
- wip [`286bd61`](https://git.odit.services/lfk/frontend/commit/286bd614976dcf8bcb14cffd092f23ef65393917)
- wip [`b89d4f2`](https://git.odit.services/lfk/frontend/commit/b89d4f248c5575548d77336832c64dc6e395efc3)
- inputElementID param [`4d79589`](https://git.odit.services/lfk/frontend/commit/4d79589903bb0726f6bcb2c0e5089a9e20f7db17)
- wip [`53f5fa3`](https://git.odit.services/lfk/frontend/commit/53f5fa3988e81215e17e41b7dd92e9ddf897610a)
- wip [`444b1f5`](https://git.odit.services/lfk/frontend/commit/444b1f537016b303a57fcaaac4468a749fe4f33c)
- disable autocomplete [`72e5425`](https://git.odit.services/lfk/frontend/commit/72e5425c0847102b0ed3f88abe17dc22ccea0a30)
#### [1.13.5](https://git.odit.services/lfk/frontend/compare/1.13.4...1.13.5) #### [1.13.5](https://git.odit.services/lfk/frontend/compare/1.13.4...1.13.5)
> 20 May 2025
- add missing cursor-pointer [`6500839`](https://git.odit.services/lfk/frontend/commit/650083965a35cf3b05b6b67389ff8035dc5fa3fa) - add missing cursor-pointer [`6500839`](https://git.odit.services/lfk/frontend/commit/650083965a35cf3b05b6b67389ff8035dc5fa3fa)
- refactor(DonationsOverview): drop checkboxes - they dont do anything [`06d22c9`](https://git.odit.services/lfk/frontend/commit/06d22c929f94587d9bdbcb4abfc0a770cf94a771) - refactor(DonationsOverview): drop checkboxes - they dont do anything [`06d22c9`](https://git.odit.services/lfk/frontend/commit/06d22c929f94587d9bdbcb4abfc0a770cf94a771)
- chore(release): 1.13.5 [`e2a1c9a`](https://git.odit.services/lfk/frontend/commit/e2a1c9a508c6061e55438afefcd641e3d9423aaa)
#### [1.13.4](https://git.odit.services/lfk/frontend/compare/1.13.3...1.13.4) #### [1.13.4](https://git.odit.services/lfk/frontend/compare/1.13.3...1.13.4)

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@odit/lfk-frontend", "name": "@odit/lfk-frontend",
"version": "1.14.0", "version": "1.13.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"i18n-order": "node order.js", "i18n-order": "node order.js",

View File

@ -5,8 +5,8 @@
DonorService, DonorService,
RunnerService, RunnerService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import Select from "svelte-select";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
import VirtualSelect from "./VirtualSelect.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
let runners = []; let runners = [];
@ -53,32 +53,37 @@
loadDonors(); loadDonors();
const getRunnerLabel = (option) => { const getRunnerLabel = (option) => {
return [option.firstname,option.middlename,option.lastname].join(" ").replace(" "," ") + " [#"+option.id+"]";
}
const filterRunners = (label, filterText, option) => {
if (filterText.startsWith("#")) {
return option.value.id == parseInt(filterText.replace("#", ""));
}
return ( return (
[option.firstname, option.middlename, option.lastname] label.toLowerCase().includes(filterText.toLowerCase()) ||
.join(" ") option.value.toString().startsWith(filterText.toLowerCase())
.replace(" ", " ") +
" [#" +
option.id +
"]"
); );
}; };
let selectRefRunner;
let selectRefDonor;
function resetAll() { function resetAll() {
runnerinfo = { id: 0, firstname: "", lastname: "" }; runnerinfo = { id: 0, firstname: "", lastname: "" };
donorinfo = { id: 0, firstname: "", lastname: "" }; donorinfo = { id: 0, firstname: "", lastname: "" };
amount = null; amount = null;
address_checked = false; address_checked = false;
donor_create_new = false; donor_create_new = false;
selectRefRunner?.reset(); const clears = document.querySelectorAll(".clearSelect");
selectRefDonor?.reset(); clears.forEach(c => {
document.querySelector("#jjqzqicxujrnnh1x3447x18x").focus(); c.click();
}
onMount(() => {
document.querySelector("#jjqzqicxujrnnh1x3447x18x").focus();
}); });
setTimeout(() => {
document.querySelector("#wrapper_runner_select input").focus();
}, 50);
}
onMount(() => {
document.querySelector("#wrapper_runner_select input").focus();
})
</script> </script>
<div class="p-4"> <div class="p-4">
@ -97,32 +102,24 @@
</div> </div>
{/if} {/if}
<!-- --> <!-- Runner Selection -->
<div id="wrapper_runner_select">
<h4 class="text-xl font-semibold">{$_("runner")}</h4> <h4 class="text-xl font-semibold">{$_("runner")}</h4>
<VirtualSelect <Select
inputElementID="jjqzqicxujrnnh1x3447x18x" containerClasses="rounded-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
bind:this={selectRefRunner} itemFilter={(label, filterText, option) =>
on:onClear={() => { filterRunners(label, filterText, option)}
console.log("Cleared selection"); items={runners}
}} showChevron={true}
options={runners} placeholder={$_("search-for-runner-by-name-or-id")}
filterFn={(item, searchTerm) => { noOptionsMessage={$_("no-runners-found")}
if (searchTerm.startsWith("#")) { on:select={(selectedValue) => {
const id = parseInt(searchTerm.replace("#", "")); runnerinfo = selectedValue.detail.value;
return item.value.id === id;
}
return item.label.toLowerCase().includes(searchTerm.toLowerCase());
}}
bind:selected={runnerinfo}
inputAriaLabel={$_("search-for-runner-by-name-or-id")}
inputPlaceholder={$_("search-for-runner-by-name-or-id")}
noOptionsText={$_("no-runners-found")}
on:onSelected={(data) => {
if (data.detail !== null) {
document.querySelector("#donation_amount_eur").focus(); document.querySelector("#donation_amount_eur").focus();
}
}} }}
on:clear={() => (runnerinfo = { id: 0, firstname: "", lastname: "" })}
/> />
</div>
<!-- Amount Input --> <!-- Amount Input -->
<div> <div>
@ -134,7 +131,8 @@
class:focus:border-red-500={!amount > 0} class:focus:border-red-500={!amount > 0}
class:focus:ring-red-500={!amount > 0} class:focus:ring-red-500={!amount > 0}
bind:value={amount} bind:value={amount}
on:keydown={(e) => { on:keydown={(e)=>
{
if(e.key==="Enter"){ if(e.key==="Enter"){
e.preventDefault(); e.preventDefault();
document.querySelector("#button_existing_donor").focus(); document.querySelector("#button_existing_donor").focus();
@ -162,16 +160,13 @@
<div class="mb-2"> <div class="mb-2">
<div class="flex border rounded-md overflow-hidden shadow-sm"> <div class="flex border rounded-md overflow-hidden shadow-sm">
<button <button
on:keydown={(e) => { on:keydown={(e)=>
{
if(e.key==="ArrowRight"){ if(e.key==="ArrowRight"){
e.preventDefault(); e.preventDefault();
document.querySelector("#button_new_donor").focus(); document.querySelector("#button_new_donor").focus();
document.querySelector("#button_new_donor").click(); document.querySelector("#button_new_donor").click();
} }
if (e.key === "Enter") {
e.preventDefault();
document.querySelector("#zt12c3udy3bme5bqobmqcif1").focus();
}
}} }}
id="button_existing_donor" id="button_existing_donor"
class:bg-indigo-600={!donor_create_new} class:bg-indigo-600={!donor_create_new}
@ -185,25 +180,19 @@
{$_("existing-donor")} {$_("existing-donor")}
</button> </button>
<button <button
on:keydown={(e) => { on:keydown={(e)=>
{
if(e.key==="ArrowLeft"){ if(e.key==="ArrowLeft"){
e.preventDefault(); e.preventDefault();
document.querySelector("#button_existing_donor").focus(); document.querySelector("#button_existing_donor").focus();
document.querySelector("#button_existing_donor").click(); document.querySelector("#button_existing_donor").click();
} }
if (e.key === "Enter") {
e.preventDefault();
document.querySelector("#button_new_donor").click();
}
}} }}
id="button_new_donor" id="button_new_donor"
class={`py-2 px-4 w-1/2 transition-colors ${donor_create_new ? "bg-indigo-600 text-white" : "bg-gray-100 text-gray-700"}`} class={`py-2 px-4 w-1/2 transition-colors ${donor_create_new ? "bg-indigo-600 text-white" : "bg-gray-100 text-gray-700"}`}
on:click={() => { on:click={() => {
donor_create_new = true; donor_create_new = true;
donorinfo = { id: 0, firstname: "", lastname: "" }; donorinfo = { id: 0, firstname: "", lastname: "" };
setTimeout(() => {
document.querySelector("#firstname").focus();
}, 50);
}} }}
> >
{$_("new-donor")} {$_("new-donor")}
@ -212,31 +201,19 @@
</div> </div>
{#if !donor_create_new} {#if !donor_create_new}
<VirtualSelect <Select
inputElementID="zt12c3udy3bme5bqobmqcif1" containerClasses="rounded-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
bind:this={selectRefDonor} itemFilter={(label, filterText, option) =>
on:onClear={() => { filterRunners(label, filterText, option)}
console.log("Cleared selection"); items={donors}
}} showChevron={true}
options={donors} placeholder={$_("search-for-donor")}
filterFn={(item, searchTerm) => { noOptionsMessage={$_("no-donors-found")}
return item.label on:select={(selectedValue) => {
.toLowerCase() donorinfo = selectedValue.detail.value;
.includes(searchTerm.toLowerCase());
}}
bind:selected={donorinfo}
inputAriaLabel={$_("search-for-donor")}
inputPlaceholder={$_("search-for-donor")}
noOptionsText={$_("no-donors-found")}
on:onSelected={(data) => {
console.log(data.detail);
if (data.detail !== null) {
document.querySelector("#submit_button").focus();
setTimeout(() => {
document.querySelector("#submit_button").focus();
}, 100);
}
}} }}
on:clear={() =>
(donorinfo = { id: 0, firstname: "", lastname: "" })}
/> />
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
@ -251,11 +228,6 @@
<input <input
type="text" type="text"
id="firstname" id="firstname"
on:keydown={(e) => {
if (e.key === "Enter") {
document.querySelector("#lastname").focus();
}
}}
bind:value={donorinfo.firstname} bind:value={donorinfo.firstname}
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-neutral-300 border bg-neutral-50 text-neutral-800 p-2" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-neutral-300 border bg-neutral-50 text-neutral-800 p-2"
placeholder={$_("first-name")} placeholder={$_("first-name")}

View File

@ -1,357 +0,0 @@
<script>
import { createEventDispatcher, onMount, tick } from "svelte";
// Generate a default unique ID
function generateDefaultID() {
return "virtual-select-" + Math.random().toString(36).slice(2);
}
// Props
export let options = [];
export let selected = null;
export let inputPlaceholder = "Search options...";
export let noOptionsText = "No options found";
export let inputAriaLabel = "Search and select an option";
export let toggleAriaLabel = "Toggle dropdown";
export let clearAriaLabel = "Clear selection";
export let filterFn = null; // Custom filter function
export let autofocus = false; // Autofocus input
export let inputElementID = generateDefaultID(); // Input element ID
// Internal state
let searchTerm = "";
let filteredOptions = options;
let isOpen = false;
let container;
let visibleItems = [];
let startIndex = 0;
let itemHeight = 40; // Fixed height for each option (in pixels)
let visibleCount = 10; // Default number of items to render
let focusedIndex = -1; // Track the focused option index (-1 means no focus)
let inputElement; // Reference to input element
const dispatch = createEventDispatcher();
// Filter options based on search term
$: {
filteredOptions = searchTerm
? filterFn
? options.filter((option) => filterFn(option, searchTerm))
: options.filter((option) =>
option.label.toLowerCase().includes(searchTerm.toLowerCase())
)
: options;
// Reset scroll and focus when filtered options change
startIndex = 0;
focusedIndex = -1;
updateVisibleItems();
}
// Update visible items based on scroll position
function updateVisibleItems() {
if (!container) return;
const scrollTop = container.scrollTop;
startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + visibleCount,
filteredOptions.length
);
visibleItems = filteredOptions.slice(startIndex, endIndex);
}
// Handle scroll event
function handleScroll() {
updateVisibleItems();
}
// Calculate visible item count based on container height
async function updateVisibleCount() {
if (container) {
await tick(); // Wait for DOM to render
visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2; // Buffer of 2 items
updateVisibleItems();
}
}
// Handle option selection
function selectOption(option) {
selected = option.value;
isOpen = false;
searchTerm = option.label; // Set searchTerm to the selected option's label
focusedIndex = -1;
dispatch("onSelected", option.value);
}
// Handle clear selection
function clearSelection() {
selected = null;
searchTerm = "";
focusedIndex = -1;
dispatch("onSelected", null);
dispatch("onClear");
}
// Reset component state
export function reset() {
selected = null;
searchTerm = "";
isOpen = false;
focusedIndex = -1;
startIndex = 0;
updateVisibleItems();
dispatch("onSelected", null);
dispatch("onClear");
}
// Toggle dropdown
async function toggleDropdown() {
isOpen = !isOpen;
if (isOpen) {
forceVisibleItems();
}
}
// Handle click outside to close dropdown
function handleClickOutside(event) {
if (!event.target.closest(".select-container")) {
isOpen = false;
focusedIndex = -1;
}
}
// Handle input focus to open dropdown
async function handleInputFocus() {
// forceVisibleItems();
}
// Handle input typing to open dropdown
async function handleInput() {
forceVisibleItems();
}
async function forceVisibleItems() {
isOpen = true;
await updateVisibleCount(); // Ensure items render on focus
// these 2 timeouts are a more or less tmp fix for rendering items when dropdown opens
setTimeout(async () => {
await updateVisibleCount(); // Ensure items render on focus
}, 25);
setTimeout(async () => {
await updateVisibleCount(); // Ensure items render on focus
}, 50);
}
// Handle keyboard navigation
function handleKeydown(event, index) {
if (!isOpen) return;
if (event.key === "ArrowDown") {
event.preventDefault();
if (focusedIndex < filteredOptions.length - 1) {
focusedIndex += 1;
scrollToFocusedItem();
}
} else if (event.key === "ArrowUp") {
event.preventDefault();
if (focusedIndex > 0) {
focusedIndex -= 1;
scrollToFocusedItem();
} else if (focusedIndex === -1 && filteredOptions.length > 0) {
focusedIndex = 0;
scrollToFocusedItem();
}
} else if (event.key === "Enter" && index >= 0) {
event.preventDefault();
selectOption(filteredOptions[index]);
} else if (event.key === "Escape") {
event.preventDefault();
isOpen = false;
focusedIndex = -1;
}
}
// Scroll to the focused item
function scrollToFocusedItem() {
if (!container || focusedIndex < 0) return;
const itemTop = focusedIndex * itemHeight;
const itemBottom = itemTop + itemHeight;
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.clientHeight;
if (itemTop < containerTop) {
container.scrollTop = itemTop;
} else if (itemBottom > containerBottom) {
container.scrollTop = itemBottom - container.clientHeight;
}
updateVisibleItems();
}
// Initialize container size observer and autofocus fallback
onMount(async () => {
if (container) {
const resizeObserver = new ResizeObserver(updateVisibleCount);
resizeObserver.observe(container);
return () => resizeObserver.disconnect();
}
// Fallback autofocus with tick to ensure inputElement is bound
if (autofocus && inputElement) {
await tick();
inputElement.focus();
}
});
// Get display text for the input
function getDisplayText() {
if (!selected) return inputPlaceholder;
const selectedOption = options.find((option) => option.value === selected);
return selectedOption ? selectedOption.label : inputPlaceholder;
}
</script>
<svelte:window on:click={handleClickOutside} />
<div class="select-container relative w-full">
<!-- Select element with inline search -->
<div
class="border rounded-md px-3 py-2 bg-white shadow-sm flex items-center gap-2"
role="combobox"
aria-expanded={isOpen}
>
<input
autocomplete="off"
type="text"
id={inputElementID}
bind:value={searchTerm}
bind:this={inputElement}
placeholder={getDisplayText()}
class="w-full bg-transparent focus:outline-none {selected
? 'text-black'
: 'text-gray-700'}"
{autofocus}
on:focus={handleInputFocus}
on:input={handleInput}
on:keydown={(e) => {
if (e.key === "Enter" && !isOpen) {
toggleDropdown();
} else {
handleKeydown(e, focusedIndex);
}
}}
aria-label={inputAriaLabel}
/>
{#if selected}
<button
type="button"
class="w-5 h-5 flex items-center justify-center text-gray-500 hover:text-gray-700"
on:click={clearSelection}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
clearSelection();
} else if (e.key === "Escape") {
e.preventDefault();
isOpen = false;
focusedIndex = -1;
}
}}
role="button"
tabindex="0"
aria-label={clearAriaLabel}
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/if}
<svg
class="w-4 h-4 text-gray-500 transform {isOpen ? 'rotate-180' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
on:click={toggleDropdown}
role="button"
tabindex="0"
on:keydown={(e) => {
if (e.key === "Enter") toggleDropdown();
else if (e.key === "Escape") {
e.preventDefault();
isOpen = false;
focusedIndex = -1;
}
}}
aria-label={toggleAriaLabel}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke
Politeness="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
<!-- Dropdown -->
{#if isOpen}
<div
class="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg max-h-80 overflow-auto"
bind:this={container}
on:scroll={handleScroll}
role="listbox"
>
{#if filteredOptions.length > 0}
<!-- Virtualized list container -->
<div style="height: {filteredOptions.length * itemHeight}px;">
<div style="transform: translateY({startIndex * itemHeight}px);">
{#each visibleItems as item, i (item.label + "-" + (startIndex + i))}
<div
class="px-3 py-2 hover:bg-blue-100 cursor-pointer {selected ===
item.value
? 'bg-blue-50'
: ''} {focusedIndex === startIndex + i
? 'bg-blue-200 outline outline-2 outline-blue-500'
: ''}"
on:click={() => selectOption(item)}
on:keydown={(e) => handleKeydown(e, startIndex + i)}
role="option"
tabindex="0"
aria-selected={selected === item.value}
>
{item.label}
</div>
{/each}
</div>
</div>
{:else}
<div class="px-3 py-2 text-gray-500">{noOptionsText}</div>
{/if}
</div>
{/if}
</div>
<style>
/* Ensure Tailwind classes handle additional styling */
:global(.select-container input:focus) {
border-color: #3b82f6; /* Tailwind's blue-500 */
}
:global([role="option"]:focus) {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
:global([role="button"]:focus) {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
</style>

View File

@ -227,7 +227,7 @@
"enabled_large": "Aktiviert", "enabled_large": "Aktiviert",
"english": "Englisch", "english": "Englisch",
"enter-payment": "Zahlung eingeben", "enter-payment": "Zahlung eingeben",
"error-creating-donation": "Fehler beim Erstellen des Sponsorings", "error-creating-donation": "Fehler bei der Anlage",
"error-during-import": "Fehler beim Importieren", "error-during-import": "Fehler beim Importieren",
"error-whyile-copying-to-clipboard": "Fehler beim Kopieren in die Zwischenablage", "error-whyile-copying-to-clipboard": "Fehler beim Kopieren in die Zwischenablage",
"error_on_login": "😢Fehler beim Login", "error_on_login": "😢Fehler beim Login",

View File

@ -227,7 +227,6 @@
"enabled_large": "Enabled", "enabled_large": "Enabled",
"english": "English", "english": "English",
"enter-payment": "Enter payment", "enter-payment": "Enter payment",
"error-creating-donation": "error creating the sponsoring",
"error-during-import": "Error during import", "error-during-import": "Error during import",
"error-whyile-copying-to-clipboard": "Error while copying to clipboard", "error-whyile-copying-to-clipboard": "Error while copying to clipboard",
"error_on_login": "Error on login", "error_on_login": "Error on login",