update to svelte 5
All checks were successful
delpoy / build-and-deploy (push) Successful in 35s

This commit is contained in:
2024-12-02 00:28:43 +01:00
parent abffa440a1
commit 95968148a6
53 changed files with 2199 additions and 2002 deletions

View File

@@ -2,10 +2,7 @@
import { onDestroy } from 'svelte';
import { env } from '$env/dynamic/public';
// start date in milliseconds. if undefined, start will be Date.now
export let start: number | undefined = undefined;
// end date in milliseconds
export let end: number;
let { start, end }: { start?: number; end: number } = $props();
let title = `Spielstart ist am ${new Date(env.PUBLIC_START_DATE).toLocaleString('de-DE', {
day: '2-digit',
@@ -26,7 +23,7 @@
];
}
let [days, hours, minutes, seconds] = getUntil();
let [days, hours, minutes, seconds] = $state(getUntil());
let intervalId = setInterval(() => {
[days, hours, minutes, seconds] = getUntil();
if (start) start += 1000;
@@ -41,25 +38,25 @@
>
<div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}>
<span class="countdown font-mono text-3xl sm:text-6xl">
<span class="m-auto" style="--value:{days};" />
<span class="m-auto" style="--value:{days};"></span>
</span>
Tage
</div>
<div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}>
<span class="countdown font-mono text-3xl sm:text-6xl">
<span class="m-auto" style="--value:{hours};" />
<span class="m-auto" style="--value:{hours};"></span>
</span>
Stunden
</div>
<div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}>
<span class="countdown font-mono text-3xl sm:text-6xl">
<span class="m-auto" style="--value:{minutes};" />
<span class="m-auto" style="--value:{minutes};"></span>
</span>
Minuten
</div>
<div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}>
<span class="countdown font-mono text-3xl sm:text-6xl">
<span class="m-auto" style="--value:{seconds};" />
<span class="m-auto" style="--value:{seconds};"></span>
</span>
Sekunden
</div>

View File

@@ -1,6 +1,5 @@
<script lang="ts">
export let size = '24';
export let fill = 'currentColor';
let { size = '24', fill = 'currentColor' } = $props();
</script>
<svg xmlns="http://www.w3.org/2000/svg" height={size} width={size} {fill} viewBox="0 0 512 512"

View File

@@ -1,6 +1,5 @@
<script lang="ts">
export let size = '24';
export let fill = 'currentColor';
let { size = '24', fill = 'currentColor' } = $props();
</script>
<svg xmlns="http://www.w3.org/2000/svg" height={size} width={size} {fill} viewBox="0 0 512 512"

View File

@@ -1,12 +1,17 @@
<script lang="ts">
// eslint-disable-next-line no-undef
type T = $$Generic;
export let id: string | null = null;
export let name: string | null = null;
export let disabled = false;
export let available: string[] | { [key: string]: T } = {};
export let value: T[] = [];
let {
id,
name,
disabled = false,
available = {},
value = $bindable([])
}: {
id?: string;
name?: string;
disabled?: boolean;
available?: string[] | { [key: string]: any };
value: any[];
} = $props();
</script>
<div class="flex items-center gap-4">
@@ -15,7 +20,7 @@
{name}
class="select select-bordered select-xs"
disabled={disabled || available.length === 0}
on:change={(e) => {
onchange={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
value.push(Object.values(available)[Object.keys(available).indexOf(e.target.value)]);
@@ -42,7 +47,7 @@
<button
{disabled}
class:pointer-events-none={disabled}
on:click={() => {
onclick={() => {
value.splice(i, 1);
value = value;
}}></button

View File

@@ -1,30 +1,46 @@
<script lang="ts">
import { Eye, EyeSlash } from 'svelte-heros-v2';
import { createEventDispatcher } from 'svelte';
import type { Snippet } from 'svelte';
export let id: string | null = null;
export let name: string | null = null;
export let type = 'text';
export let value: string | null = null;
export let placeholder: string | null = null;
export let pattern: RegExp | null = null;
export let required = false;
export let disabled = false;
export let readonly = false;
export let checked = false;
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
export let pickyWidth = true;
export let containerClass = '';
export let inputElement: HTMLInputElement | undefined = undefined;
const dispatch = createEventDispatcher();
function input(e: Event & { currentTarget: EventTarget & HTMLInputElement }) {
dispatch('input', e);
}
function click(e: Event) {
dispatch('click', e);
}
let {
label,
notice,
id,
name,
type = 'text',
value = $bindable(),
placeholder,
pattern,
required = false,
disabled = false,
readonly = false,
checked = false,
size = 'md',
pickyWidth = true,
containerClass = '',
inputElement = $bindable(),
oninput,
onclick
}: {
label?: Snippet;
notice?: Snippet;
id?: string;
name?: string;
type?: string;
value?: string;
placeholder?: string;
pattern?: RegExp;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
checked?: boolean;
size?: 'xs' | 'sm' | 'md' | 'lg';
pickyWidth?: boolean;
containerClass?: string;
inputElement?: HTMLInputElement;
oninput?: (e: Event & { currentTarget: EventTarget & HTMLInputElement }) => void;
onclick?: (e: Event) => void;
} = $props();
let initialType = type;
@@ -50,15 +66,15 @@
{disabled}
bind:value
bind:this={inputElement}
on:input={input}
on:click={click}
{oninput}
{onclick}
/>
{:else}
<div>
{#if $$slots.label}
{#if label}
<label class="label" for={id}>
<span class="label-text">
<slot name="label" />
{@render label()}
{#if required}
<span class="text-red-700">*</span>
{/if}
@@ -93,31 +109,23 @@
{readonly}
bind:this={inputElement}
autocomplete="off"
on:input={(e) => {
value = e.target?.value;
if (pattern && !pattern.test(e.target?.value)) {
if (inputElement?.value.endsWith(e.data)) {
value = e.target?.value.substring(0, e.target?.value.length - e.data.length);
}
return;
}
return input(e);
oninput={(e: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
value = e.currentTarget.value;
if (pattern && !pattern.test(value)) return;
oninput && oninput(e);
}}
on:paste={(e) => {
if (
pattern &&
!pattern.test((e.clipboardData || window.clipboardData).getData('text'))
) {
onpaste={(e) => {
if (pattern && e.clipboardData && !pattern.test(e.clipboardData.getData('text'))) {
e.preventDefault();
}
}}
on:click={click}
{onclick}
/>
{#if initialType === 'password'}
<button
class="absolute right-3"
type="button"
on:click={() => {
onclick={() => {
type = type === 'password' ? 'text' : 'password';
}}
>
@@ -129,9 +137,11 @@
</button>
{/if}
</div>
{#if $$slots.notice}
{#if notice}
<label class="label" for={id}>
<span class="label-text-alt"><slot name="notice" /></span>
<span class="label-text-alt">
{@render notice()}
</span>
</label>
{/if}
</div>

View File

@@ -1,23 +1,34 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
let {
id,
value = $bindable(),
inputValue = $bindable(),
suggestionRequired = false,
emptyAllowed = false,
searchSuggestionFunc = () => Promise.resolve([]),
invalidMessage,
size = 'md',
label,
required = false,
onsubmit
}: {
id?: string;
value?: string;
inputValue?: string;
suggestionRequired?: boolean;
emptyAllowed?: boolean;
searchSuggestionFunc?: (input: string) => Promise<{ name: string; value: string }[]>;
invalidMessage?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
label?: string;
required?: boolean;
onsubmit?: (event: Event & { input: string; value: string }) => void;
} = $props();
export let id: string | null = null;
export let value = '';
export let inputValue = '';
export let suggestionRequired = false;
export let emptyAllowed = false;
export let searchSuggestionFunc: (
input: string
) => Promise<{ name: string; value: string }[]> = () => Promise.resolve([]);
export let invalidMessage: string | null = null;
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
export let label: string | null = null;
export let required = false;
const dispatch = createEventDispatcher();
let searchSuggestions: { name: string; value: string }[] = [];
$: if (!suggestionRequired) value = inputValue;
let searchSuggestions: { name: string; value: string }[] = $state([]);
$effect(() => {
if (!suggestionRequired) value = inputValue;
});
</script>
<div class="relative">
@@ -42,7 +53,7 @@
{id}
{required}
bind:value={inputValue}
on:input={(e) => {
oninput={(e: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
value = '';
searchSuggestionFunc(inputValue).then((v) => {
searchSuggestions = v;
@@ -51,15 +62,15 @@
inputValue = searchSuggestion.name;
value = searchSuggestion.value;
searchSuggestions = [];
e.target?.setCustomValidity('');
dispatch('submit', { input: inputValue, value: value });
e.currentTarget.setCustomValidity('');
onsubmit && onsubmit(Object.assign(e, { input: inputValue, value: value }));
} else if (inputValue === '' && emptyAllowed) {
dispatch('submit', { input: '', value: '' });
onsubmit && onsubmit(Object.assign(e, { input: '', value: '' }));
}
});
}}
on:invalid={(e) => {
if (invalidMessage != null) e.target?.setCustomValidity(invalidMessage);
oninvalid={(e: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
invalidMessage && e.currentTarget.setCustomValidity(invalidMessage);
}}
pattern={suggestionRequired
? `${value ? inputValue : 'a^' + (emptyAllowed ? '|$^' : '')}`
@@ -74,11 +85,11 @@
<button
class="block w-full overflow-hidden text-ellipsis whitespace-nowrap"
title="{searchSuggestion.name} ({searchSuggestion.value})"
on:click|preventDefault={() => {
onclick={(e) => {
inputValue = searchSuggestion.name;
value = searchSuggestion.value;
searchSuggestions = [];
dispatch('submit', { input: inputValue, value: value });
onsubmit && onsubmit(Object.assign(e, { input: inputValue, value: value }));
}}>{searchSuggestion.name}</button
>
</li>
@@ -90,7 +101,8 @@
<!-- close the search suggestions box when clicking outside -->
{#if inputValue && searchSuggestions.length !== 0}
<button
aria-label=" "
class="absolute top-0 left-0 z-10 w-full h-full cursor-default"
on:click={() => (searchSuggestions = [])}
/>
onclick={() => (searchSuggestions = [])}
></button>
{/if}

View File

@@ -1,20 +1,31 @@
<script lang="ts">
// eslint-disable-next-line no-undef
import { createEventDispatcher } from 'svelte';
import type { Snippet } from 'svelte';
type T = $$Generic;
export let id: string | null = null;
export let name: string | null = null;
export let value: T | null = null;
export let label: string | null = null;
export let notice: string | null = null;
export let required = false;
export let disabled = false;
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
export let pickyWidth = true;
let dispatch = createEventDispatcher();
let {
children,
id,
name,
value = $bindable(),
label,
notice,
required = false,
disabled = false,
size = 'md',
pickyWidth = true,
onChange
}: {
children: Snippet;
id?: string;
name?: string;
value?: any;
label?: string;
notice?: string;
required?: boolean;
disabled?: boolean;
size?: 'xs' | 'sm' | 'md' | 'lg';
pickyWidth?: boolean;
onChange?: ({ value }: { value: any }) => void;
} = $props();
</script>
<div>
@@ -40,9 +51,9 @@
{required}
{disabled}
bind:value
on:change={(e) => dispatch('change', { value: value })}
onchange={() => onChange && onChange({ value: value })}
>
<slot />
{@render children()}
</select>
{#if notice}
<label class="label" for={id}>

View File

@@ -41,7 +41,7 @@
{rows}
bind:value
on:click={(e) => dispatch('click', e)}
/>
></textarea>
{#if notice}
<label class="label" for={id}>
<span class="label-text-alt">{notice}</span>

View File

@@ -1,13 +1,11 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
import { onMount, type Snippet, tick } from 'svelte';
export let onUpdate: () => Promise<any> = Promise.resolve;
let { children, onUpdate }: { children: Snippet; onUpdate: () => Promise<any> } = $props();
let bodyElem: HTMLTableSectionElement;
let intersectionElem: HTMLElement;
let intersectionObserver: IntersectionObserver;
let intersectionElement: HTMLElement;
async function getIntersectionElement(): Promise<HTMLElement> {
if (!bodyElem.lastElementChild) {
await new Promise<void>((resolve) => {
@@ -26,10 +24,10 @@
await onUpdate();
await tick();
intersectionObserver = new IntersectionObserver(
const intersectionObserver = new IntersectionObserver(
async (entries, observer) => {
if (entries.filter((e) => e.isIntersecting).length === 0 || !entries) return;
observer.unobserve(intersectionElement);
observer.unobserve(intersectionElem);
const rows = bodyElem.rows.length;
@@ -37,7 +35,7 @@
await tick();
if (rows === bodyElem.rows.length) return;
observer.observe((intersectionElement = await getIntersectionElement()));
observer.observe((intersectionElem = await getIntersectionElement()));
},
{ threshold: 0.25 }
);
@@ -51,14 +49,14 @@
return;
}
if (intersectionElement) intersectionObserver.unobserve(intersectionElement);
intersectionObserver.observe((intersectionElement = await getIntersectionElement()));
if (intersectionElem) intersectionObserver.unobserve(intersectionElem);
intersectionObserver.observe((intersectionElem = await getIntersectionElement()));
}).observe(bodyElem, { childList: true });
intersectionObserver.observe((intersectionElement = await getIntersectionElement()));
intersectionObserver.observe((intersectionElem = await getIntersectionElement()));
});
</script>
<tbody bind:this={bodyElem}>
<slot />
{@render children()}
</tbody>

View File

@@ -1,17 +1,20 @@
<script lang="ts">
import { createEventDispatcher, getContext, onDestroy } from 'svelte';
import { getContext, onDestroy, type Snippet } from 'svelte';
import type { Writable } from 'svelte/store';
import { ChevronDown, ChevronUp } from 'svelte-heros-v2';
let { children, onSort }: { children: Snippet; onSort: ({ asc }: { asc: boolean }) => void } =
$props();
let id = crypto.randomUUID();
let asc = false;
let asc = $state(false);
let { ascHeader } = getContext('sortableTr') as { ascHeader: Writable<null | string> };
ascHeader.subscribe((v) => {
if (v !== id) asc = false;
});
let dispatch = createEventDispatcher();
onDestroy(() => {
if ($ascHeader === id) $ascHeader = null;
});
@@ -20,12 +23,14 @@
<th>
<button
class="flex flex-center"
on:click={() => {
dispatch('sort', { asc: (asc = !asc) });
onclick={() => {
onSort({ asc: (asc = !asc) });
$ascHeader = id;
}}
>
<span class="mr-1"><slot /></span>
<span class="mr-1">
{@render children()}
</span>
{#if $ascHeader === id && asc}
<ChevronUp variation="solid" />
{:else}

View File

@@ -1,12 +1,14 @@
<script lang="ts">
import { setContext } from 'svelte';
import { setContext, type Snippet } from 'svelte';
import { writable } from 'svelte/store';
let { children, ...restProps }: { children: Snippet; [x: string]: unknown } = $props();
setContext('sortableTr', {
ascHeader: writable(null)
});
</script>
<tr {...$$restProps}>
<slot />
<tr {...restProps}>
{@render children()}
</tr>

View File

@@ -3,15 +3,10 @@
import { fly } from 'svelte/transition';
import { onDestroy } from 'svelte';
export let timeout = 2000;
export let show = false;
let { children, timeout = 2000, show = false } = $props();
export function reset() {
progressValue = 1;
}
let progressValue = 100;
let intervalClear: ReturnType<typeof setInterval> | undefined;
let progressValue = $state(100);
let intervalClear: ReturnType<typeof setInterval> | undefined = $state();
function startTimout() {
intervalClear = setInterval(() => {
@@ -23,10 +18,11 @@
}, timeout / 100);
}
$: if (show) {
$effect(() => {
if (!show) return;
progressValue = 0;
startTimout();
}
});
onDestroy(() => clearInterval(intervalClear));
</script>
@@ -36,23 +32,23 @@
in:fly={{ x: 0, duration: 200 }}
out:fly={{ x: 400, duration: 400 }}
class="toast"
on:mouseenter={() => {
onmouseenter={() => {
clearInterval(intervalClear);
progressValue = 1;
}}
on:mouseleave={startTimout}
onmouseleave={startTimout}
role="alert"
>
<div class="alert alert-error border-none relative text-gray-900 overflow-hidden">
<div class="flex gap-2 z-10">
<ExclamationCircle />
<slot />
{@render children()}
</div>
<progress
class="progress progress-error absolute bottom-0 h-[3px] w-full bg-[rgba(0,0,0,0.6)]"
value={progressValue}
max="100"
/>
></progress>
</div>
</div>
{/if}