This commit is contained in:
97
src/components/admin/search/Search.svelte
Normal file
97
src/components/admin/search/Search.svelte
Normal 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>
|
||||
61
src/components/admin/search/TeamSearch.svelte
Normal file
61
src/components/admin/search/TeamSearch.svelte
Normal 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)}
|
||||
/>
|
||||
61
src/components/admin/search/UserSearch.svelte
Normal file
61
src/components/admin/search/UserSearch.svelte
Normal 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)}
|
||||
/>
|
||||
Reference in New Issue
Block a user