Compare commits
	
		
			22 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						878d3acc9c
	
				 | 
					
					
						|||
| 
						
						
							
						
						5a7bc239d2
	
				 | 
					
					
						|||
| 
						
						
							
						
						661a698fba
	
				 | 
					
					
						|||
| 
						
						
							
						
						1b088b87bf
	
				 | 
					
					
						|||
| 
						
						
							
						
						d5fecd3f31
	
				 | 
					
					
						|||
| 
						
						
							
						
						e9938a5472
	
				 | 
					
					
						|||
| 
						
						
							
						
						77413c7e53
	
				 | 
					
					
						|||
| 
						
						
							
						
						72e5425c08
	
				 | 
					
					
						|||
| 
						
						
							
						
						53f5fa3988
	
				 | 
					
					
						|||
| 
						
						
							
						
						6ef6dc0078
	
				 | 
					
					
						|||
| 
						
						
							
						
						b89d4f248c
	
				 | 
					
					
						|||
| 
						
						
							
						
						444b1f5370
	
				 | 
					
					
						|||
| 
						
						
							
						
						3709881176
	
				 | 
					
					
						|||
| 
						
						
							
						
						a00af08b3f
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ef34359d8
	
				 | 
					
					
						|||
| 
						
						
							
						
						4d79589903
	
				 | 
					
					
						|||
| 
						
						
							
						
						1386b80d0c
	
				 | 
					
					
						|||
| 
						
						
							
						
						286bd61497
	
				 | 
					
					
						|||
| 
						
						
							
						
						50b5e4e455
	
				 | 
					
					
						|||
| 
						
						
							
						
						2c91f46375
	
				 | 
					
					
						|||
| 
						
						
							
						
						0cb1193269
	
				 | 
					
					
						|||
| 
						
						
							
						
						564a971c63
	
				 | 
					
					
						
							
								
								
									
										36
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -2,10 +2,46 @@
 | 
			
		||||
 | 
			
		||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
 | 
			
		||||
 | 
			
		||||
#### [1.14.2](https://git.odit.services/lfk/frontend/compare/1.14.1...1.14.2)
 | 
			
		||||
 | 
			
		||||
- feat(GenerateRunnerCertificates): support skipping runners without scans [`5a7bc23`](https://git.odit.services/lfk/frontend/commit/5a7bc239d2f93ced9ebdc5b113fe27d0d8d3899c)
 | 
			
		||||
 | 
			
		||||
#### [1.14.1](https://git.odit.services/lfk/frontend/compare/1.14.0...1.14.1)
 | 
			
		||||
 | 
			
		||||
> 26 May 2025
 | 
			
		||||
 | 
			
		||||
- fix: ensure numeric values are parsed as integers in DocumentServer methods [`1b088b8`](https://git.odit.services/lfk/frontend/commit/1b088b87bf6e67796c2509d9c21f21833cb4df0f)
 | 
			
		||||
- chore(release): 1.14.1 [`661a698`](https://git.odit.services/lfk/frontend/commit/661a698fbaeb2432bec758ed632a520676ae86b2)
 | 
			
		||||
 | 
			
		||||
#### [1.14.0](https://git.odit.services/lfk/frontend/compare/1.13.5...1.14.0)
 | 
			
		||||
 | 
			
		||||
> 20 May 2025
 | 
			
		||||
 | 
			
		||||
- 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)
 | 
			
		||||
- chore(release): 1.14.0 [`d5fecd3`](https://git.odit.services/lfk/frontend/commit/d5fecd3f31916b80c305d76f37c4600f1d242eba)
 | 
			
		||||
- 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)
 | 
			
		||||
 | 
			
		||||
> 20 May 2025
 | 
			
		||||
 | 
			
		||||
- 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)
 | 
			
		||||
- 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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
 | 
			
		||||
  <body>
 | 
			
		||||
    <span style="display: none; visibility: hidden" id="buildinfo"
 | 
			
		||||
      >RELEASE_INFO-1.13.5-RELEASE_INFO</span
 | 
			
		||||
      >RELEASE_INFO-1.14.2-RELEASE_INFO</span
 | 
			
		||||
    >
 | 
			
		||||
    <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
			
		||||
    <script src="/env.js"></script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@odit/lfk-frontend",
 | 
			
		||||
  "version": "1.13.5",
 | 
			
		||||
  "version": "1.14.2",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "i18n-order": "node order.js",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
class DocumentServer {
 | 
			
		||||
  baseUrl: string;
 | 
			
		||||
  apiKey: string;
 | 
			
		||||
@@ -12,19 +13,19 @@ class DocumentServer {
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < cards.length; i++) {
 | 
			
		||||
      const card = {
 | 
			
		||||
        id: cards[i].id,
 | 
			
		||||
        id: parseInt(cards[i].id),
 | 
			
		||||
        enabled: cards[i].enabled,
 | 
			
		||||
        code: cards[i].code,
 | 
			
		||||
        runner: {
 | 
			
		||||
          id: cards[i]?.runner?.id,
 | 
			
		||||
          id: parseInt(cards[i]?.runner?.id),
 | 
			
		||||
          first_name: cards[i]?.runner?.firstname,
 | 
			
		||||
          middle_name: cards[i]?.runner?.middlename,
 | 
			
		||||
          last_name: cards[i]?.runner?.lastname,
 | 
			
		||||
          group: {
 | 
			
		||||
            id: cards[i]?.runner?.group.id,
 | 
			
		||||
            id: parseInt(cards[i]?.runner?.group?.id),
 | 
			
		||||
            name: cards[i]?.runner?.group.name,
 | 
			
		||||
            parent_group: {
 | 
			
		||||
              id: cards[i]?.runner?.group?.parentGroup?.id,
 | 
			
		||||
              id: parseInt(cards[i]?.runner?.group?.parentGroup?.id),
 | 
			
		||||
              name: cards[i]?.runner?.group?.parentGroup?.name,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
@@ -57,15 +58,15 @@ class DocumentServer {
 | 
			
		||||
    for (let i = 0; i < runners.length; i++) {
 | 
			
		||||
      console.log(runners[i]);
 | 
			
		||||
      const card = {
 | 
			
		||||
        id: runners[i].id,
 | 
			
		||||
        id: parseInt(runners[i].id),
 | 
			
		||||
        first_name: runners[i].firstname,
 | 
			
		||||
        middle_name: runners[i].middlename,
 | 
			
		||||
        last_name: runners[i].lastname,
 | 
			
		||||
        group: {
 | 
			
		||||
          id: runners[i].group.id,
 | 
			
		||||
          id: parseInt(runners[i].group.id),
 | 
			
		||||
          name: runners[i].group.name,
 | 
			
		||||
          parent_group: {
 | 
			
		||||
            id: runners[i]?.group?.parentGroup?.id,
 | 
			
		||||
            id: parseInt(runners[i]?.group?.parentGroup?.id),
 | 
			
		||||
            name: runners[i]?.group?.parentGroup?.name,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
@@ -96,28 +97,28 @@ class DocumentServer {
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < runners.length; i++) {
 | 
			
		||||
      const certificate = {
 | 
			
		||||
        id: runners[i].id,
 | 
			
		||||
        id: parseInt(runners[i].id),
 | 
			
		||||
        first_name: runners[i].firstname,
 | 
			
		||||
        middle_name: runners[i].middlename,
 | 
			
		||||
        last_name: runners[i].lastname,
 | 
			
		||||
        self_service_link: runners[i].selfserviceLink,
 | 
			
		||||
        group: {
 | 
			
		||||
          id: runners[i].group.id,
 | 
			
		||||
          id: parseInt(runners[i].group.id),
 | 
			
		||||
          name: runners[i].group.name,
 | 
			
		||||
          parent_group: {
 | 
			
		||||
            id: runners[i]?.group?.parentGroup?.id,
 | 
			
		||||
            id: parseInt(runners[i]?.group?.parentGroup?.id),
 | 
			
		||||
            name: runners[i]?.group?.parentGroup?.name,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        distance: runners[i].distance,
 | 
			
		||||
        distance: parseInt(runners[i].distance),
 | 
			
		||||
        distance_donations: runners[i].distanceDonations.map(
 | 
			
		||||
          (distanceDonation: any) => {
 | 
			
		||||
            return {
 | 
			
		||||
              id: distanceDonation.id,
 | 
			
		||||
              amount: distanceDonation.amount,
 | 
			
		||||
              amount_per_distance: distanceDonation.amountPerDistance,
 | 
			
		||||
              amount: parseInt(distanceDonation.amount),
 | 
			
		||||
              amount_per_distance: parseInt(distanceDonation.amountPerDistance),
 | 
			
		||||
              donor: {
 | 
			
		||||
                id: distanceDonation.donor.id,
 | 
			
		||||
                id: parseInt(distanceDonation.donor.id),
 | 
			
		||||
                first_name: distanceDonation.donor.firstname,
 | 
			
		||||
                middle_name: distanceDonation.donor.middlename,
 | 
			
		||||
                last_name: distanceDonation.donor.lastname,
 | 
			
		||||
 
 | 
			
		||||
@@ -20,13 +20,13 @@
 | 
			
		||||
  export let generate_orgs = [];
 | 
			
		||||
  export let generate_teams = [];
 | 
			
		||||
 | 
			
		||||
  function generateCertificates(locale) {
 | 
			
		||||
  function generateCertificates(locale, include0runners = false) {
 | 
			
		||||
    if (generate_orgs.length > 0) {
 | 
			
		||||
      generateOrgCertificates(locale);
 | 
			
		||||
      generateOrgCertificates(locale, include0runners = false);
 | 
			
		||||
    } else if (generate_teams.length > 0) {
 | 
			
		||||
      generateTeamCertificates(locale);
 | 
			
		||||
      generateTeamCertificates(locale, include0runners = false);
 | 
			
		||||
    } else {
 | 
			
		||||
      generateRunnerCertificates(locale);
 | 
			
		||||
      generateRunnerCertificates(locale, include0runners = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  function download(blob, fileName) {
 | 
			
		||||
@@ -41,7 +41,7 @@
 | 
			
		||||
    toast.success($_("pdf-successfully-generated"));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function generateRunnerCertificates(locale) {
 | 
			
		||||
  async function generateRunnerCertificates(locale, include0runners = false) {
 | 
			
		||||
    toast.loading($_("generating-pdf"));
 | 
			
		||||
    const current_donations =
 | 
			
		||||
      (await DonationService.donationControllerGetAll()) || [];
 | 
			
		||||
@@ -50,7 +50,15 @@
 | 
			
		||||
	  const linkRunner = await RunnerService.runnerControllerGetOne(runner.id)
 | 
			
		||||
      linkRunner.distanceDonations =
 | 
			
		||||
        current_donations.filter((d) => d.runner?.id == runner.id) || [];
 | 
			
		||||
      certificateRunners.push(linkRunner);
 | 
			
		||||
      // check if linkRunner.distance is 0, if so, and include0runners is false, skip this runner
 | 
			
		||||
      if (
 | 
			
		||||
        !include0runners &&
 | 
			
		||||
        (linkRunner.distance === 0 || linkRunner.distance === null)
 | 
			
		||||
      ) {
 | 
			
		||||
        continue;
 | 
			
		||||
      } else {
 | 
			
		||||
        certificateRunners.push(linkRunner);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    documentServer
 | 
			
		||||
      .generateCertificates(certificateRunners, locale)
 | 
			
		||||
@@ -66,7 +74,7 @@
 | 
			
		||||
      .catch((err) => {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function generateTeamCertificates(locale) {
 | 
			
		||||
  async function generateTeamCertificates(locale, include0runners = false) {
 | 
			
		||||
    toast.loading($_("generating-pdfs"));
 | 
			
		||||
    let count = 0;
 | 
			
		||||
    const current_donations =
 | 
			
		||||
@@ -80,7 +88,15 @@
 | 
			
		||||
      for (let runner of runners) {
 | 
			
		||||
        runner.distanceDonations =
 | 
			
		||||
          current_donations.filter((d) => d.runner?.id == runner.id) || [];
 | 
			
		||||
        certificateRunners.push(runner);
 | 
			
		||||
        // check if runner.distance is 0, if so, and include0runners is false, skip this runner
 | 
			
		||||
        if (
 | 
			
		||||
          !include0runners &&
 | 
			
		||||
          (runner.distance === 0 || runner.distance === null)
 | 
			
		||||
        ) {
 | 
			
		||||
          continue;
 | 
			
		||||
        } else {
 | 
			
		||||
          certificateRunners.push(runner);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      documentServer
 | 
			
		||||
        .generateCertificates(certificateRunners, locale)
 | 
			
		||||
@@ -95,7 +111,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function generateOrgCertificates(locale) {
 | 
			
		||||
  async function generateOrgCertificates(locale, include0runners = false) {
 | 
			
		||||
    toast.loading($_("generating-pdfs"));
 | 
			
		||||
    const current_donations =
 | 
			
		||||
      (await DonationService.donationControllerGetAll()) || [];
 | 
			
		||||
@@ -114,7 +130,15 @@
 | 
			
		||||
      for (let runner of runners) {
 | 
			
		||||
        runner.distanceDonations =
 | 
			
		||||
          current_donations.filter((d) => d.runner?.id == runner.id) || [];
 | 
			
		||||
        certificateRunners.push(runner);
 | 
			
		||||
          // check if runner.distance is 0, if so, and include0runners is false, skip this runner
 | 
			
		||||
          if (
 | 
			
		||||
            !include0runners &&
 | 
			
		||||
            (runner.distance === 0 || runner.distance === null)
 | 
			
		||||
          ) {
 | 
			
		||||
            continue;
 | 
			
		||||
          } else {
 | 
			
		||||
            certificateRunners.push(runner);
 | 
			
		||||
          }
 | 
			
		||||
      }
 | 
			
		||||
      await documentServer
 | 
			
		||||
        .generateCertificates(certificateRunners, locale)
 | 
			
		||||
@@ -161,20 +185,36 @@
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if certificates_show}
 | 
			
		||||
  <button
 | 
			
		||||
    on:click={() => {
 | 
			
		||||
      generateCertificates("de");
 | 
			
		||||
    }}
 | 
			
		||||
    class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
 | 
			
		||||
  >
 | 
			
		||||
    {$_("generate-runner-certificates")}: DE
 | 
			
		||||
  </button>
 | 
			
		||||
  <button
 | 
			
		||||
    on:click={() => {
 | 
			
		||||
      generateCertificates("en");
 | 
			
		||||
    }}
 | 
			
		||||
    class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
 | 
			
		||||
  >
 | 
			
		||||
    {$_("generate-runner-certificates")}: EN
 | 
			
		||||
  </button>
 | 
			
		||||
	<button
 | 
			
		||||
		on:click={() => {
 | 
			
		||||
			generateCertificates("de", true);
 | 
			
		||||
		}}
 | 
			
		||||
		class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
 | 
			
		||||
	>
 | 
			
		||||
		{$_("generate-runner-certificates")}: DE
 | 
			
		||||
	</button>
 | 
			
		||||
	<button
 | 
			
		||||
		on:click={() => {
 | 
			
		||||
			generateCertificates("de", false);
 | 
			
		||||
		}}
 | 
			
		||||
		class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
 | 
			
		||||
	>
 | 
			
		||||
		{$_("generate-runner-certificates")}: DE [{$_('exclude_0m_runners_certificate')}]
 | 
			
		||||
	</button>
 | 
			
		||||
	<button
 | 
			
		||||
		on:click={() => {
 | 
			
		||||
			generateCertificates("en", true);
 | 
			
		||||
		}}
 | 
			
		||||
		class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
 | 
			
		||||
	>
 | 
			
		||||
		{$_("generate-runner-certificates")}: EN
 | 
			
		||||
	</button>
 | 
			
		||||
	<button
 | 
			
		||||
		on:click={() => {
 | 
			
		||||
			generateCertificates("en", false);
 | 
			
		||||
		}}
 | 
			
		||||
		class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
 | 
			
		||||
	>
 | 
			
		||||
		{$_("generate-runner-certificates")}: EN [{$_('exclude_0m_runners_certificate')}]
 | 
			
		||||
	</button>
 | 
			
		||||
{/if}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,394 +1,422 @@
 | 
			
		||||
<script>
 | 
			
		||||
  import { _ } from "svelte-i18n";
 | 
			
		||||
  import {
 | 
			
		||||
    DonationService,
 | 
			
		||||
    DonorService,
 | 
			
		||||
    RunnerService,
 | 
			
		||||
  } from "@odit/lfk-client-js";
 | 
			
		||||
  import Select from "svelte-select";
 | 
			
		||||
  import toast from "svelte-french-toast";
 | 
			
		||||
	import { _ } from "svelte-i18n";
 | 
			
		||||
	import {
 | 
			
		||||
		DonationService,
 | 
			
		||||
		DonorService,
 | 
			
		||||
		RunnerService,
 | 
			
		||||
	} from "@odit/lfk-client-js";
 | 
			
		||||
	import toast from "svelte-french-toast";
 | 
			
		||||
	import VirtualSelect from "./VirtualSelect.svelte";
 | 
			
		||||
	import { onMount } from "svelte";
 | 
			
		||||
 | 
			
		||||
  let runners = [];
 | 
			
		||||
  let donors = [];
 | 
			
		||||
  let runnerinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
  let donorinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
  let address = {
 | 
			
		||||
    address1: "",
 | 
			
		||||
    address2: "",
 | 
			
		||||
    city: "",
 | 
			
		||||
    postalcode: "",
 | 
			
		||||
    country: "Germany",
 | 
			
		||||
  };
 | 
			
		||||
  let amount = null;
 | 
			
		||||
  let address_checked = false;
 | 
			
		||||
  let donor_create_new = false;
 | 
			
		||||
  let last_created = null;
 | 
			
		||||
	let runners = [];
 | 
			
		||||
	let donors = [];
 | 
			
		||||
	let runnerinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
	let donorinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
	let address = {
 | 
			
		||||
		address1: "",
 | 
			
		||||
		address2: "",
 | 
			
		||||
		city: "",
 | 
			
		||||
		postalcode: "",
 | 
			
		||||
		country: "Germany",
 | 
			
		||||
	};
 | 
			
		||||
	let amount = null;
 | 
			
		||||
	let address_checked = false;
 | 
			
		||||
	let donor_create_new = false;
 | 
			
		||||
	let last_created = null;
 | 
			
		||||
 | 
			
		||||
  RunnerService.runnerControllerGetAll()
 | 
			
		||||
    .then((val) => {
 | 
			
		||||
      runners = val.map((r) => {
 | 
			
		||||
        return { label: getRunnerLabel(r), value: r };
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .catch((err) => {
 | 
			
		||||
      console.log("error fetching runners:", err);
 | 
			
		||||
    });
 | 
			
		||||
	RunnerService.runnerControllerGetAll()
 | 
			
		||||
		.then((val) => {
 | 
			
		||||
			runners = val.map((r) => {
 | 
			
		||||
				return { label: getRunnerLabel(r), value: r };
 | 
			
		||||
			});
 | 
			
		||||
		})
 | 
			
		||||
		.catch((err) => {
 | 
			
		||||
			console.log("error fetching runners:", err);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
  function loadDonors() {
 | 
			
		||||
    DonorService.donorControllerGetAll()
 | 
			
		||||
      .then((val) => {
 | 
			
		||||
        donors = val.map((r) => {
 | 
			
		||||
          return { label: getRunnerLabel(r), value: r };
 | 
			
		||||
        });
 | 
			
		||||
        console.log("refreshed donors");
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          loadDonors;
 | 
			
		||||
        }, 30000);
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        console.log("error fetching donors:", err);
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
  loadDonors();
 | 
			
		||||
	function loadDonors() {
 | 
			
		||||
		DonorService.donorControllerGetAll()
 | 
			
		||||
			.then((val) => {
 | 
			
		||||
				donors = val.map((r) => {
 | 
			
		||||
					return { label: getRunnerLabel(r), value: r };
 | 
			
		||||
				});
 | 
			
		||||
				console.log("refreshed donors");
 | 
			
		||||
				setTimeout(() => {
 | 
			
		||||
					loadDonors;
 | 
			
		||||
				}, 30000);
 | 
			
		||||
			})
 | 
			
		||||
			.catch((err) => {
 | 
			
		||||
				console.log("error fetching donors:", err);
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
	loadDonors();
 | 
			
		||||
 | 
			
		||||
  const getRunnerLabel = (option) => {
 | 
			
		||||
    return [option.firstname,option.middlename,option.lastname].join(" ").replace("  "," ") + " [#"+option.id+"]";
 | 
			
		||||
  }
 | 
			
		||||
	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 (
 | 
			
		||||
      label.toLowerCase().includes(filterText.toLowerCase()) ||
 | 
			
		||||
      option.value.toString().startsWith(filterText.toLowerCase())
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
	let selectRefRunner;
 | 
			
		||||
	let selectRefDonor;
 | 
			
		||||
 | 
			
		||||
  function resetAll() {
 | 
			
		||||
    runnerinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
    donorinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
    amount = null;
 | 
			
		||||
    address_checked = false;
 | 
			
		||||
    donor_create_new = false;
 | 
			
		||||
    const clears = document.querySelectorAll(".clearSelect");
 | 
			
		||||
    clears.forEach(c => {
 | 
			
		||||
      c.click();
 | 
			
		||||
    });
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      document.querySelector("#wrapper_runner_select input").focus();
 | 
			
		||||
    }, 50);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    document.querySelector("#wrapper_runner_select input").focus();
 | 
			
		||||
  })
 | 
			
		||||
	function resetAll() {
 | 
			
		||||
		runnerinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
		donorinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
		amount = null;
 | 
			
		||||
		address_checked = false;
 | 
			
		||||
		donor_create_new = false;
 | 
			
		||||
		selectRefRunner?.reset();
 | 
			
		||||
		selectRefDonor?.reset();
 | 
			
		||||
		document.querySelector("#jjqzqicxujrnnh1x3447x18x").focus();
 | 
			
		||||
	}
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		document.querySelector("#jjqzqicxujrnnh1x3447x18x").focus();
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="p-4">
 | 
			
		||||
  <h3 class="text-3xl font-bold">{$_("fast_donation_create")}</h3>
 | 
			
		||||
  <!--  -->
 | 
			
		||||
  <div>
 | 
			
		||||
    <div class="w-full space-y-4 mb-6">
 | 
			
		||||
      {#if last_created}
 | 
			
		||||
        <div class="mt-4 p-3 bg-green-50 border border-green-200 rounded-md">
 | 
			
		||||
          <p class="text-black">
 | 
			
		||||
            {$_("last-created-donation")}: #{last_created.id}: {last_created.amountPerDistance /
 | 
			
		||||
              100} € für {getRunnerLabel(last_created.runner)} von {getRunnerLabel(
 | 
			
		||||
              last_created.donor
 | 
			
		||||
            )}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
      {/if}
 | 
			
		||||
	<h3 class="text-3xl font-bold">{$_("fast_donation_create")}</h3>
 | 
			
		||||
	<!--  -->
 | 
			
		||||
	<div>
 | 
			
		||||
		<div class="w-full space-y-4 mb-6">
 | 
			
		||||
			{#if last_created}
 | 
			
		||||
				<div class="mt-4 p-3 bg-green-50 border border-green-200 rounded-md">
 | 
			
		||||
					<p class="text-black">
 | 
			
		||||
						{$_("last-created-donation")}: #{last_created.id}: {last_created.amountPerDistance /
 | 
			
		||||
							100}€ für {getRunnerLabel(last_created.runner)} von {getRunnerLabel(
 | 
			
		||||
							last_created.donor
 | 
			
		||||
						)}
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
 | 
			
		||||
      <!-- Runner Selection -->
 | 
			
		||||
      <div id="wrapper_runner_select">
 | 
			
		||||
        <h4 class="text-xl font-semibold">{$_("runner")}</h4>
 | 
			
		||||
        <Select
 | 
			
		||||
          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"
 | 
			
		||||
          itemFilter={(label, filterText, option) =>
 | 
			
		||||
            filterRunners(label, filterText, option)}
 | 
			
		||||
          items={runners}
 | 
			
		||||
          showChevron={true}
 | 
			
		||||
          placeholder={$_("search-for-runner-by-name-or-id")}
 | 
			
		||||
          noOptionsMessage={$_("no-runners-found")}
 | 
			
		||||
          on:select={(selectedValue) => {
 | 
			
		||||
            runnerinfo = selectedValue.detail.value;
 | 
			
		||||
            document.querySelector("#donation_amount_eur").focus();
 | 
			
		||||
          }}
 | 
			
		||||
          on:clear={() => (runnerinfo = { id: 0, firstname: "", lastname: "" })}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
			<!--  -->
 | 
			
		||||
			<h4 class="text-xl font-semibold">{$_("runner")}</h4>
 | 
			
		||||
			<VirtualSelect
 | 
			
		||||
				inputElementID="jjqzqicxujrnnh1x3447x18x"
 | 
			
		||||
				bind:this={selectRefRunner}
 | 
			
		||||
				on:onClear={() => {
 | 
			
		||||
					console.log("Cleared selection");
 | 
			
		||||
				}}
 | 
			
		||||
				options={runners}
 | 
			
		||||
				filterFn={(item, searchTerm) => {
 | 
			
		||||
					if (searchTerm.startsWith("#")) {
 | 
			
		||||
						const id = parseInt(searchTerm.replace("#", ""));
 | 
			
		||||
						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();
 | 
			
		||||
					}
 | 
			
		||||
				}}
 | 
			
		||||
			/>
 | 
			
		||||
 | 
			
		||||
      <!-- Amount Input -->
 | 
			
		||||
      <div>
 | 
			
		||||
        <h4 class="text-xl font-semibold">{$_("amount-per-kilometer")}</h4>
 | 
			
		||||
        <div class="mt-1 flex rounded-md shadow-sm">
 | 
			
		||||
          <input
 | 
			
		||||
            autocomplete="off"
 | 
			
		||||
            class:border-red-500={!amount > 0}
 | 
			
		||||
            class:focus:border-red-500={!amount > 0}
 | 
			
		||||
            class:focus:ring-red-500={!amount > 0}
 | 
			
		||||
            bind:value={amount}
 | 
			
		||||
            on:keydown={(e)=>
 | 
			
		||||
            {
 | 
			
		||||
              if(e.key==="Enter"){
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                document.querySelector("#button_existing_donor").focus();
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            type="number"
 | 
			
		||||
            step="0.01"
 | 
			
		||||
            id="donation_amount_eur"
 | 
			
		||||
            name="donation_amount_eur"
 | 
			
		||||
            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="z.B. 1,50"
 | 
			
		||||
          />
 | 
			
		||||
          <span
 | 
			
		||||
            class="inline-flex items-center px-3 rounded-r-md border border-neutral-300 bg-neutral-50 text-neutral-500 text-sm"
 | 
			
		||||
            >€</span
 | 
			
		||||
          >
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
			<!-- Amount Input -->
 | 
			
		||||
			<div>
 | 
			
		||||
				<h4 class="text-xl font-semibold">{$_("amount-per-kilometer")}</h4>
 | 
			
		||||
				<div class="mt-1 flex rounded-md shadow-sm">
 | 
			
		||||
					<input
 | 
			
		||||
						autocomplete="off"
 | 
			
		||||
						class:border-red-500={!amount > 0}
 | 
			
		||||
						class:focus:border-red-500={!amount > 0}
 | 
			
		||||
						class:focus:ring-red-500={!amount > 0}
 | 
			
		||||
						bind:value={amount}
 | 
			
		||||
						on:keydown={(e) => {
 | 
			
		||||
							if (e.key === "Enter") {
 | 
			
		||||
								e.preventDefault();
 | 
			
		||||
								document.querySelector("#button_existing_donor").focus();
 | 
			
		||||
							}
 | 
			
		||||
						}}
 | 
			
		||||
						type="number"
 | 
			
		||||
						step="0.01"
 | 
			
		||||
						id="donation_amount_eur"
 | 
			
		||||
						name="donation_amount_eur"
 | 
			
		||||
						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="z.B. 1,50"
 | 
			
		||||
					/>
 | 
			
		||||
					<span
 | 
			
		||||
						class="inline-flex items-center px-3 rounded-r-md border border-neutral-300 bg-neutral-50 text-neutral-500 text-sm"
 | 
			
		||||
						>€</span
 | 
			
		||||
					>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
      <!-- Donor Selection -->
 | 
			
		||||
      <div>
 | 
			
		||||
        <h4 class="text-xl font-semibold">{$_("donor")}</h4>
 | 
			
		||||
			<!-- Donor Selection -->
 | 
			
		||||
			<div>
 | 
			
		||||
				<h4 class="text-xl font-semibold">{$_("donor")}</h4>
 | 
			
		||||
 | 
			
		||||
        <!-- Donor Type Toggle -->
 | 
			
		||||
        <div class="mb-2">
 | 
			
		||||
          <div class="flex border rounded-md overflow-hidden shadow-sm">
 | 
			
		||||
            <button
 | 
			
		||||
            on:keydown={(e)=>
 | 
			
		||||
            {
 | 
			
		||||
              if(e.key==="ArrowRight"){
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                document.querySelector("#button_new_donor").focus();
 | 
			
		||||
                document.querySelector("#button_new_donor").click();
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            id="button_existing_donor"
 | 
			
		||||
            class:bg-indigo-600={!donor_create_new}
 | 
			
		||||
            class:text-white={!donor_create_new}
 | 
			
		||||
            class="py-2 px-4 w-1/2 transition-colors"
 | 
			
		||||
            on:click={() => {
 | 
			
		||||
              donor_create_new = false;
 | 
			
		||||
              donorinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
            }}
 | 
			
		||||
            >
 | 
			
		||||
            {$_("existing-donor")}
 | 
			
		||||
          </button>
 | 
			
		||||
          <button
 | 
			
		||||
              on:keydown={(e)=>
 | 
			
		||||
              {
 | 
			
		||||
                if(e.key==="ArrowLeft"){
 | 
			
		||||
                  e.preventDefault();
 | 
			
		||||
                  document.querySelector("#button_existing_donor").focus();
 | 
			
		||||
                  document.querySelector("#button_existing_donor").click();
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
              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"}`}
 | 
			
		||||
              on:click={() => {
 | 
			
		||||
                donor_create_new = true;
 | 
			
		||||
                donorinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {$_("new-donor")}
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
				<!-- Donor Type Toggle -->
 | 
			
		||||
				<div class="mb-2">
 | 
			
		||||
					<div class="flex border rounded-md overflow-hidden shadow-sm">
 | 
			
		||||
						<button
 | 
			
		||||
							on:keydown={(e) => {
 | 
			
		||||
								if (e.key === "ArrowRight") {
 | 
			
		||||
									e.preventDefault();
 | 
			
		||||
									document.querySelector("#button_new_donor").focus();
 | 
			
		||||
									document.querySelector("#button_new_donor").click();
 | 
			
		||||
								}
 | 
			
		||||
								if (e.key === "Enter") {
 | 
			
		||||
									e.preventDefault();
 | 
			
		||||
									document.querySelector("#zt12c3udy3bme5bqobmqcif1").focus();
 | 
			
		||||
								}
 | 
			
		||||
							}}
 | 
			
		||||
							id="button_existing_donor"
 | 
			
		||||
							class:bg-indigo-600={!donor_create_new}
 | 
			
		||||
							class:text-white={!donor_create_new}
 | 
			
		||||
							class="py-2 px-4 w-1/2 transition-colors"
 | 
			
		||||
							on:click={() => {
 | 
			
		||||
								donor_create_new = false;
 | 
			
		||||
								donorinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
							}}
 | 
			
		||||
						>
 | 
			
		||||
							{$_("existing-donor")}
 | 
			
		||||
						</button>
 | 
			
		||||
						<button
 | 
			
		||||
							on:keydown={(e) => {
 | 
			
		||||
								if (e.key === "ArrowLeft") {
 | 
			
		||||
									e.preventDefault();
 | 
			
		||||
									document.querySelector("#button_existing_donor").focus();
 | 
			
		||||
									document.querySelector("#button_existing_donor").click();
 | 
			
		||||
								}
 | 
			
		||||
								if (e.key === "Enter") {
 | 
			
		||||
									e.preventDefault();
 | 
			
		||||
									document.querySelector("#button_new_donor").click();
 | 
			
		||||
								}
 | 
			
		||||
							}}
 | 
			
		||||
							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"}`}
 | 
			
		||||
							on:click={() => {
 | 
			
		||||
								donor_create_new = true;
 | 
			
		||||
								donorinfo = { id: 0, firstname: "", lastname: "" };
 | 
			
		||||
								setTimeout(() => {
 | 
			
		||||
									document.querySelector("#firstname").focus();
 | 
			
		||||
								}, 50);
 | 
			
		||||
							}}
 | 
			
		||||
						>
 | 
			
		||||
							{$_("new-donor")}
 | 
			
		||||
						</button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
        {#if !donor_create_new}
 | 
			
		||||
          <Select
 | 
			
		||||
            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"
 | 
			
		||||
            itemFilter={(label, filterText, option) =>
 | 
			
		||||
              filterRunners(label, filterText, option)}
 | 
			
		||||
            items={donors}
 | 
			
		||||
            showChevron={true}
 | 
			
		||||
            placeholder={$_("search-for-donor")}
 | 
			
		||||
            noOptionsMessage={$_("no-donors-found")}
 | 
			
		||||
            on:select={(selectedValue) => {
 | 
			
		||||
              donorinfo = selectedValue.detail.value;
 | 
			
		||||
            }}
 | 
			
		||||
            on:clear={() =>
 | 
			
		||||
              (donorinfo = { id: 0, firstname: "", lastname: "" })}
 | 
			
		||||
          />
 | 
			
		||||
        {:else}
 | 
			
		||||
          <div class="space-y-3">
 | 
			
		||||
            <!-- First Name -->
 | 
			
		||||
            <div>
 | 
			
		||||
              <label
 | 
			
		||||
                for="firstname"
 | 
			
		||||
                class="block text-sm font-medium text-gray-700"
 | 
			
		||||
              >
 | 
			
		||||
                {$_("first-name")}
 | 
			
		||||
              </label>
 | 
			
		||||
              <input
 | 
			
		||||
                type="text"
 | 
			
		||||
                id="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"
 | 
			
		||||
                placeholder={$_("first-name")}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
				{#if !donor_create_new}
 | 
			
		||||
					<VirtualSelect
 | 
			
		||||
						inputElementID="zt12c3udy3bme5bqobmqcif1"
 | 
			
		||||
						bind:this={selectRefDonor}
 | 
			
		||||
						on:onClear={() => {
 | 
			
		||||
							console.log("Cleared selection");
 | 
			
		||||
						}}
 | 
			
		||||
						options={donors}
 | 
			
		||||
						filterFn={(item, searchTerm) => {
 | 
			
		||||
							return item.label
 | 
			
		||||
								.toLowerCase()
 | 
			
		||||
								.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);
 | 
			
		||||
							}
 | 
			
		||||
						}}
 | 
			
		||||
					/>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<div class="space-y-3">
 | 
			
		||||
						<!-- First Name -->
 | 
			
		||||
						<div>
 | 
			
		||||
							<label
 | 
			
		||||
								for="firstname"
 | 
			
		||||
								class="block text-sm font-medium text-gray-700"
 | 
			
		||||
							>
 | 
			
		||||
								{$_("first-name")}
 | 
			
		||||
							</label>
 | 
			
		||||
							<input
 | 
			
		||||
								type="text"
 | 
			
		||||
								id="firstname"
 | 
			
		||||
								on:keydown={(e) => {
 | 
			
		||||
									if (e.key === "Enter") {
 | 
			
		||||
										document.querySelector("#lastname").focus();
 | 
			
		||||
									}
 | 
			
		||||
								}}
 | 
			
		||||
								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"
 | 
			
		||||
								placeholder={$_("first-name")}
 | 
			
		||||
							/>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
            <!-- Last Name -->
 | 
			
		||||
            <div>
 | 
			
		||||
              <label
 | 
			
		||||
                for="lastname"
 | 
			
		||||
                class="block text-sm font-medium text-gray-700"
 | 
			
		||||
              >
 | 
			
		||||
                {$_("last-name")}
 | 
			
		||||
              </label>
 | 
			
		||||
              <input
 | 
			
		||||
                type="text"
 | 
			
		||||
                id="lastname"
 | 
			
		||||
                bind:value={donorinfo.lastname}
 | 
			
		||||
                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={$_("last-name")}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
						<!-- Last Name -->
 | 
			
		||||
						<div>
 | 
			
		||||
							<label
 | 
			
		||||
								for="lastname"
 | 
			
		||||
								class="block text-sm font-medium text-gray-700"
 | 
			
		||||
							>
 | 
			
		||||
								{$_("last-name")}
 | 
			
		||||
							</label>
 | 
			
		||||
							<input
 | 
			
		||||
								type="text"
 | 
			
		||||
								id="lastname"
 | 
			
		||||
								bind:value={donorinfo.lastname}
 | 
			
		||||
								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={$_("last-name")}
 | 
			
		||||
							/>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
            <!-- Address Checkbox -->
 | 
			
		||||
            <div class="flex items-start mt-4">
 | 
			
		||||
              <div class="flex items-center h-5">
 | 
			
		||||
                <input
 | 
			
		||||
                  id="address_check"
 | 
			
		||||
                  type="checkbox"
 | 
			
		||||
                  bind:checked={address_checked}
 | 
			
		||||
                  class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="ml-3 text-sm">
 | 
			
		||||
                <label for="address_check" class="font-medium text-gray-700">
 | 
			
		||||
                  {$_("receipt-needed")}
 | 
			
		||||
                </label>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
						<!-- Address Checkbox -->
 | 
			
		||||
						<div class="flex items-start mt-4">
 | 
			
		||||
							<div class="flex items-center h-5">
 | 
			
		||||
								<input
 | 
			
		||||
									id="address_check"
 | 
			
		||||
									type="checkbox"
 | 
			
		||||
									bind:checked={address_checked}
 | 
			
		||||
									class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
 | 
			
		||||
								/>
 | 
			
		||||
							</div>
 | 
			
		||||
							<div class="ml-3 text-sm">
 | 
			
		||||
								<label for="address_check" class="font-medium text-gray-700">
 | 
			
		||||
									{$_("receipt-needed")}
 | 
			
		||||
								</label>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
            {#if address_checked}
 | 
			
		||||
              <!-- Address Fields -->
 | 
			
		||||
              <div
 | 
			
		||||
                class="space-y-3 mt-3 p-3 border border-gray-200 rounded-md bg-gray-50"
 | 
			
		||||
              >
 | 
			
		||||
                <div>
 | 
			
		||||
                  <label
 | 
			
		||||
                    for="address1"
 | 
			
		||||
                    class="block text-sm font-medium text-gray-700"
 | 
			
		||||
                  >
 | 
			
		||||
                    {$_("address")}
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    id="address1"
 | 
			
		||||
                    bind:value={address.address1}
 | 
			
		||||
                    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"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
						{#if address_checked}
 | 
			
		||||
							<!-- Address Fields -->
 | 
			
		||||
							<div
 | 
			
		||||
								class="space-y-3 mt-3 p-3 border border-gray-200 rounded-md bg-gray-50"
 | 
			
		||||
							>
 | 
			
		||||
								<div>
 | 
			
		||||
									<label
 | 
			
		||||
										for="address1"
 | 
			
		||||
										class="block text-sm font-medium text-gray-700"
 | 
			
		||||
									>
 | 
			
		||||
										{$_("address")}
 | 
			
		||||
									</label>
 | 
			
		||||
									<input
 | 
			
		||||
										type="text"
 | 
			
		||||
										id="address1"
 | 
			
		||||
										bind:value={address.address1}
 | 
			
		||||
										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"
 | 
			
		||||
									/>
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
                <div>
 | 
			
		||||
                  <label
 | 
			
		||||
                    for="address2"
 | 
			
		||||
                    class="block text-sm font-medium text-gray-700"
 | 
			
		||||
                  >
 | 
			
		||||
                    {$_("apartment-suite-etc")}
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    id="address2"
 | 
			
		||||
                    bind:value={address.address2}
 | 
			
		||||
                    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"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
								<div>
 | 
			
		||||
									<label
 | 
			
		||||
										for="address2"
 | 
			
		||||
										class="block text-sm font-medium text-gray-700"
 | 
			
		||||
									>
 | 
			
		||||
										{$_("apartment-suite-etc")}
 | 
			
		||||
									</label>
 | 
			
		||||
									<input
 | 
			
		||||
										type="text"
 | 
			
		||||
										id="address2"
 | 
			
		||||
										bind:value={address.address2}
 | 
			
		||||
										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"
 | 
			
		||||
									/>
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
                <div class="grid grid-cols-2 gap-3">
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <label
 | 
			
		||||
                      for="postalcode"
 | 
			
		||||
                      class="block text-sm font-medium text-gray-700"
 | 
			
		||||
                    >
 | 
			
		||||
                      {$_("zip-postal-code")}
 | 
			
		||||
                    </label>
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="text"
 | 
			
		||||
                      id="postalcode"
 | 
			
		||||
                      bind:value={address.postalcode}
 | 
			
		||||
                      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"
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
								<div class="grid grid-cols-2 gap-3">
 | 
			
		||||
									<div>
 | 
			
		||||
										<label
 | 
			
		||||
											for="postalcode"
 | 
			
		||||
											class="block text-sm font-medium text-gray-700"
 | 
			
		||||
										>
 | 
			
		||||
											{$_("zip-postal-code")}
 | 
			
		||||
										</label>
 | 
			
		||||
										<input
 | 
			
		||||
											type="text"
 | 
			
		||||
											id="postalcode"
 | 
			
		||||
											bind:value={address.postalcode}
 | 
			
		||||
											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"
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <label
 | 
			
		||||
                      for="city"
 | 
			
		||||
                      class="block text-sm font-medium text-gray-700"
 | 
			
		||||
                    >
 | 
			
		||||
                      {$_("city")}
 | 
			
		||||
                    </label>
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="text"
 | 
			
		||||
                      id="city"
 | 
			
		||||
                      bind:value={address.city}
 | 
			
		||||
                      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"
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            {/if}
 | 
			
		||||
          </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- Submit Button -->
 | 
			
		||||
      <div class="mt-6">
 | 
			
		||||
        <button
 | 
			
		||||
          id="submit_button"
 | 
			
		||||
          type="button"
 | 
			
		||||
          class="w-full inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
 | 
			
		||||
          disabled={!amount > 0 ||
 | 
			
		||||
            !runnerinfo.id ||
 | 
			
		||||
            (!donorinfo.id && !donor_create_new) ||
 | 
			
		||||
            (donor_create_new &&
 | 
			
		||||
              (!donorinfo.firstname || !donorinfo.lastname)) ||
 | 
			
		||||
            (donor_create_new &&
 | 
			
		||||
              address_checked &&
 | 
			
		||||
              (!address.address1 || !address.city || !address.postalcode))}
 | 
			
		||||
          on:click={async () => {
 | 
			
		||||
            if (donor_create_new) {
 | 
			
		||||
              donorinfo = await DonorService.donorControllerPost({
 | 
			
		||||
                firstname: donorinfo.firstname,
 | 
			
		||||
                lastname: donorinfo.lastname,
 | 
			
		||||
                receiptNeeded: address_checked,
 | 
			
		||||
                ...(address_checked ? { address: address } : {}),
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
									<div>
 | 
			
		||||
										<label
 | 
			
		||||
											for="city"
 | 
			
		||||
											class="block text-sm font-medium text-gray-700"
 | 
			
		||||
										>
 | 
			
		||||
											{$_("city")}
 | 
			
		||||
										</label>
 | 
			
		||||
										<input
 | 
			
		||||
											type="text"
 | 
			
		||||
											id="city"
 | 
			
		||||
											bind:value={address.city}
 | 
			
		||||
											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"
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						{/if}
 | 
			
		||||
					</div>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
			<!-- Submit Button -->
 | 
			
		||||
			<div class="mt-6">
 | 
			
		||||
				<button
 | 
			
		||||
					id="submit_button"
 | 
			
		||||
					type="button"
 | 
			
		||||
					class="w-full inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
 | 
			
		||||
					disabled={!amount > 0 ||
 | 
			
		||||
						!runnerinfo.id ||
 | 
			
		||||
						(!donorinfo.id && !donor_create_new) ||
 | 
			
		||||
						(donor_create_new &&
 | 
			
		||||
							(!donorinfo.firstname || !donorinfo.lastname)) ||
 | 
			
		||||
						(donor_create_new &&
 | 
			
		||||
							address_checked &&
 | 
			
		||||
							(!address.address1 || !address.city || !address.postalcode))}
 | 
			
		||||
					on:click={async () => {
 | 
			
		||||
						if (donor_create_new) {
 | 
			
		||||
							donorinfo = await DonorService.donorControllerPost({
 | 
			
		||||
								firstname: donorinfo.firstname,
 | 
			
		||||
								lastname: donorinfo.lastname,
 | 
			
		||||
								receiptNeeded: address_checked,
 | 
			
		||||
								...(address_checked ? { address: address } : {}),
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
            DonationService.donationControllerPostDistance({
 | 
			
		||||
              donor: donorinfo.id,
 | 
			
		||||
              runner: runnerinfo.id,
 | 
			
		||||
              amountPerDistance: amount * 100,
 | 
			
		||||
            })
 | 
			
		||||
              .then((data) => {
 | 
			
		||||
                last_created = data;
 | 
			
		||||
                toast.success($_("donation-created-successfully"));
 | 
			
		||||
                resetAll();
 | 
			
		||||
                loadDonors();
 | 
			
		||||
              })
 | 
			
		||||
              .catch((err) => {
 | 
			
		||||
                console.error("Error creating donation:", err);
 | 
			
		||||
                toast.error($_("error-creating-donation"));
 | 
			
		||||
              });
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {$_("create")}
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
						DonationService.donationControllerPostDistance({
 | 
			
		||||
							donor: donorinfo.id,
 | 
			
		||||
							runner: runnerinfo.id,
 | 
			
		||||
							amountPerDistance: amount * 100,
 | 
			
		||||
						})
 | 
			
		||||
							.then((data) => {
 | 
			
		||||
								last_created = data;
 | 
			
		||||
								toast.success($_("donation-created-successfully"));
 | 
			
		||||
								resetAll();
 | 
			
		||||
								loadDonors();
 | 
			
		||||
							})
 | 
			
		||||
							.catch((err) => {
 | 
			
		||||
								console.error("Error creating donation:", err);
 | 
			
		||||
								toast.error($_("error-creating-donation"));
 | 
			
		||||
							});
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					{$_("create")}
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  :global(:root) {
 | 
			
		||||
    --sv-bg: #ffffff;
 | 
			
		||||
  }
 | 
			
		||||
	:global(:root) {
 | 
			
		||||
		--sv-bg: #ffffff;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										357
									
								
								src/components/tools/VirtualSelect.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										357
									
								
								src/components/tools/VirtualSelect.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,357 @@
 | 
			
		||||
<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>
 | 
			
		||||
@@ -227,11 +227,12 @@
 | 
			
		||||
    "enabled_large": "Aktiviert",
 | 
			
		||||
    "english": "Englisch",
 | 
			
		||||
    "enter-payment": "Zahlung eingeben",
 | 
			
		||||
    "error-creating-donation": "Fehler bei der Anlage",
 | 
			
		||||
    "error-creating-donation": "Fehler beim Erstellen des Sponsorings",
 | 
			
		||||
    "error-during-import": "Fehler beim Importieren",
 | 
			
		||||
    "error-whyile-copying-to-clipboard": "Fehler beim Kopieren in die Zwischenablage",
 | 
			
		||||
    "error_on_login": "😢Fehler beim Login",
 | 
			
		||||
    "everything-concerning-your-profile": "Alles zu deinem Profil",
 | 
			
		||||
    "exclude_0m_runners_certificate": "ohne 0m Läufer",
 | 
			
		||||
    "existing-donor": "Existierende Sponsor:in",
 | 
			
		||||
    "faq": "FAQ",
 | 
			
		||||
    "fast_card_replacement": "Karten-Schnellzusweisung (Mit Mobilgeräteunterstützung)",
 | 
			
		||||
 
 | 
			
		||||
@@ -227,10 +227,12 @@
 | 
			
		||||
    "enabled_large": "Enabled",
 | 
			
		||||
    "english": "English",
 | 
			
		||||
    "enter-payment": "Enter payment",
 | 
			
		||||
    "error-creating-donation": "error creating the sponsoring",
 | 
			
		||||
    "error-during-import": "Error during import",
 | 
			
		||||
    "error-whyile-copying-to-clipboard": "Error while copying to clipboard",
 | 
			
		||||
    "error_on_login": "Error on login",
 | 
			
		||||
    "everything-concerning-your-profile": "Everything concerning your profile",
 | 
			
		||||
    "exclude_0m_runners_certificate": "exclude runners without scans",
 | 
			
		||||
    "existing-donor": "Existing Donor",
 | 
			
		||||
    "faq": "FAQ",
 | 
			
		||||
    "fast_card_replacement": "Fast card replacement (with mobile support)",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user