initial commit
Some checks failed
deploy / build-and-deploy (push) Failing after 21s

This commit is contained in:
2025-05-18 13:16:20 +02:00
commit 60f3f8a096
148 changed files with 17900 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
<script lang="ts">
// types
interface Props {
id?: string;
value?: string | null;
label?: string;
readonly?: boolean;
required?: boolean;
mustMatch?: boolean;
requestSuggestions: (query: string, limit: number) => Promise<string[]>;
onSubmit?: (value: string | null) => void;
}
// html bindings
let container: HTMLDivElement;
// inputs
let { id, value = $bindable(), label, readonly, required, mustMatch, requestSuggestions, onSubmit }: Props = $props();
// states
let inputValue = $state(value);
let suggestions = $state<string[]>([]);
let matched = $state(false);
// callbacks
async function onBodyMouseDown(e: MouseEvent) {
if (!container.contains(e.target as Node)) suggestions = [];
}
async function onSearchInput() {
if (readonly) return;
suggestions = await requestSuggestions(inputValue ?? '', 5);
let suggestion = suggestions.find((s) => s === inputValue);
if (suggestion != null) {
inputValue = value = suggestion;
matched = true;
onSubmit?.(value);
} else if (!mustMatch) {
value = inputValue;
matched = false;
} else {
value = null;
matched = false;
onSubmit?.(null);
}
}
function onSuggestionClick(suggestion: string) {
inputValue = value = suggestion;
suggestions = [];
onSubmit?.(value);
}
</script>
<svelte:body onmousedown={onBodyMouseDown} />
<fieldset class="fieldset">
<legend class="fieldset-legend">
<span>
{label}
{#if required}
<span class="text-red-700">*</span>
{/if}
</span>
</legend>
<div class="relative" bind:this={container}>
<input
{id}
{readonly}
type="search"
autocomplete="off"
class="input"
bind:value={inputValue}
oninput={() => onSearchInput()}
onfocusin={() => onSearchInput()}
pattern={mustMatch && matched ? `^(${suggestions.join('|')})$` : undefined}
/>
{#if suggestions.length > 0}
<ul class="absolute bg-base-200 w-full z-20 menu menu-sm rounded-box">
{#each suggestions as suggestion (suggestion)}
<li class="w-full text-left">
<button
class="block w-full overflow-hidden text-ellipsis whitespace-nowrap"
title={suggestion}
onclick={() => onSuggestionClick(suggestion)}>{suggestion}</button
>
</li>
{/each}
</ul>
{/if}
</div>
<p class="fieldset-label"></p>
</fieldset>

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { type ActionReturnType, actions } from 'astro:actions';
import Search from '@components/admin/search/Search.svelte';
import { actionErrorPopup } from '@util/action.ts';
// types
type Teams = Exclude<ActionReturnType<typeof actions.team.teams>['data'], undefined>['teams'];
type Team = Teams[0];
interface Props {
id?: string;
value?: string | null;
label?: string;
readonly?: boolean;
required?: boolean;
mustMatch?: boolean;
onSubmit?: (team: Team | null) => void;
}
// inputs
let { id, value = $bindable(), label, readonly, required, mustMatch, onSubmit }: Props = $props();
// states
let teamSuggestionCache = $state<Teams>([]);
// functions
async function getSuggestions(query: string, limit: number) {
const { data, error } = await actions.team.teams({
name: query,
limit: limit
});
if (error) {
actionErrorPopup(error);
return [];
}
teamSuggestionCache = data.teams;
return teamSuggestionCache.map((team) => team.name);
}
async function getTeamByTeamName(teamName: string) {
let team = teamSuggestionCache.find((team) => team.name === teamName);
if (!team) {
await getSuggestions(teamName, 5);
return await getTeamByTeamName(teamName);
}
return team;
}
</script>
<Search
{id}
bind:value
{label}
{readonly}
{required}
{mustMatch}
requestSuggestions={async (teamName) => getSuggestions(teamName, 5)}
onSubmit={async (teamName) => onSubmit?.(teamName != null ? await getTeamByTeamName(teamName) : null)}
/>

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { type ActionReturnType, actions } from 'astro:actions';
import Search from '@components/admin/search/Search.svelte';
import { actionErrorPopup } from '@util/action.ts';
// types
type Users = Exclude<ActionReturnType<typeof actions.user.users>['data'], undefined>['users'];
type User = Users[0];
interface Props {
id?: string;
value?: string | null;
label?: string;
readonly?: boolean;
required?: boolean;
mustMatch?: boolean;
onSubmit?: (user: User | null) => void;
}
// inputs
let { id, value = $bindable(), label, readonly, required, mustMatch, onSubmit }: Props = $props();
// states
let userSuggestionCache = $state<Users>([]);
// functions
async function getSuggestions(query: string, limit: number) {
const { data, error } = await actions.user.users({
username: query,
limit: limit
});
if (error) {
actionErrorPopup(error);
return [];
}
userSuggestionCache = data.users;
return userSuggestionCache.map((user) => user.username);
}
async function getUserByUsername(username: string) {
let user = userSuggestionCache.find((user) => user.username === username);
if (!user) {
await getSuggestions(username, 5);
return await getUserByUsername(username);
}
return user;
}
</script>
<Search
{id}
bind:value
{label}
{readonly}
{required}
{mustMatch}
requestSuggestions={async (username) => getSuggestions(username, 5)}
onSubmit={async (username) => onSubmit?.(username != null ? await getUserByUsername(username) : null)}
/>