This commit is contained in:
81
src/components/admin/table/DataTable.svelte
Normal file
81
src/components/admin/table/DataTable.svelte
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from 'svelte/store';
|
||||
import SortableTr from '@components/admin/table/SortableTr.svelte';
|
||||
import SortableTh from '@components/admin/table/SortableTh.svelte';
|
||||
import Icon from '@iconify/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { getObjectEntryByKey } from '@util/objects.ts';
|
||||
|
||||
// types
|
||||
interface Props<T> {
|
||||
data: Writable<T[]>;
|
||||
|
||||
count?: boolean;
|
||||
keys: {
|
||||
key: string;
|
||||
label: string;
|
||||
width?: number;
|
||||
sortable?: boolean;
|
||||
transform?: Snippet<[T]>;
|
||||
}[];
|
||||
|
||||
onClick?: (t: T) => void;
|
||||
onEdit?: (t: T) => void;
|
||||
onDelete?: (t: T) => void;
|
||||
}
|
||||
|
||||
// input
|
||||
let { data, count, keys, onClick, onEdit, onDelete }: Props<any> = $props();
|
||||
</script>
|
||||
|
||||
<div class="h-screen overflow-x-auto">
|
||||
<table class="table table-pin-rows">
|
||||
<thead>
|
||||
<SortableTr {data}>
|
||||
{#if count}
|
||||
<SortableTh style="width: 5%">#</SortableTh>
|
||||
{/if}
|
||||
{#each keys as key (key.key)}
|
||||
<SortableTh style={key.width ? `width: ${key.width}%` : undefined} key={key.sortable ? key.key : undefined}
|
||||
>{key.label}</SortableTh
|
||||
>
|
||||
{/each}
|
||||
{#if onEdit || onDelete}
|
||||
<SortableTh style="width: 5%"></SortableTh>
|
||||
{/if}
|
||||
</SortableTr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $data as d, i (d)}
|
||||
<tr class="hover:bg-base-200" onclick={() => onClick?.(d)}>
|
||||
{#if count}
|
||||
<td>{i + 1}</td>
|
||||
{/if}
|
||||
{#each keys as key (key.key)}
|
||||
<td>
|
||||
{#if key.transform}
|
||||
{@render key.transform(getObjectEntryByKey(key.key, d))}
|
||||
{:else}
|
||||
{getObjectEntryByKey(key.key, d)}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
{#if onEdit || onDelete}
|
||||
<td>
|
||||
{#if onEdit}
|
||||
<button class="cursor-pointer" onclick={() => onEdit(d)}>
|
||||
<Icon icon="heroicons:pencil-square" />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onDelete}
|
||||
<button class="cursor-pointer" onclick={() => onDelete(d)}>
|
||||
<Icon icon="heroicons:trash" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
48
src/components/admin/table/SortableTh.svelte
Normal file
48
src/components/admin/table/SortableTh.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { getContext, type Snippet } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import Icon from '@iconify/svelte';
|
||||
|
||||
// types
|
||||
interface Props {
|
||||
key?: string;
|
||||
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
interface SortableHeaderContext {
|
||||
headerKey: Writable<string>;
|
||||
onSort: (key: string, order: 'asc' | 'desc') => void;
|
||||
}
|
||||
|
||||
// inputs
|
||||
const { key, children, ...restProps }: Props & Record<string, any> = $props();
|
||||
|
||||
let { headerKey, onSort }: SortableHeaderContext = getContext('sortableHeader');
|
||||
|
||||
let asc = $state(false);
|
||||
|
||||
// callbacks
|
||||
function onButtonClick() {
|
||||
if (key == undefined) return;
|
||||
|
||||
$headerKey = key;
|
||||
asc = !asc;
|
||||
onSort(key, asc ? 'asc' : 'desc');
|
||||
}
|
||||
</script>
|
||||
|
||||
<th {...restProps}>
|
||||
{#if key}
|
||||
<button class="flex items-center gap-1" onclick={() => onButtonClick()}>
|
||||
<span>{@render children?.()}</span>
|
||||
{#if $headerKey === key && asc}
|
||||
<Icon icon="heroicons:chevron-up-16-solid" />
|
||||
{:else}
|
||||
<Icon icon="heroicons:chevron-down-16-solid" />
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
</th>
|
||||
48
src/components/admin/table/SortableTr.svelte
Normal file
48
src/components/admin/table/SortableTr.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { setContext, type Snippet } from 'svelte';
|
||||
import { type Writable, writable } from 'svelte/store';
|
||||
import { getObjectEntryByKey } from '@util/objects.ts';
|
||||
|
||||
// types
|
||||
interface Props {
|
||||
data: Writable<{ [key: string]: any }[]>;
|
||||
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
// inputs
|
||||
const { data, children, ...restProps }: Props & Record<string, any> = $props();
|
||||
|
||||
setContext('sortableHeader', {
|
||||
headerKey: writable(null),
|
||||
onSort: onSort
|
||||
});
|
||||
|
||||
// functions
|
||||
function onSort(key: string, order: 'asc' | 'desc') {
|
||||
data.update((old) => {
|
||||
old.sort((a, b) => {
|
||||
let entryA = getObjectEntryByKey(key, a);
|
||||
let entryB = getObjectEntryByKey(key, b);
|
||||
|
||||
if (entryA === undefined || entryB === undefined) return 0;
|
||||
|
||||
if (typeof entryA === 'string') entryA = entryA.toLowerCase();
|
||||
if (typeof entryB === 'string') entryB = entryB.toLowerCase();
|
||||
|
||||
if (order === 'asc') {
|
||||
return entryA < entryB ? -1 : 1;
|
||||
} else if (order === 'desc') {
|
||||
return entryA > entryB ? -1 : 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return old;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr {...restProps}>
|
||||
{@render children()}
|
||||
</tr>
|
||||
Reference in New Issue
Block a user