Anton Malykhin
feat: refine leaderboard visualizations and CSV exports
236e9a2
Raw
History Blame Contribute Delete
3.07 kB
<script lang="ts" module>
import type { Snippet } from 'svelte';
export type TableColumn<Key extends string = string> = {
key: Key;
label: string | (() => string);
headClass?: string | (() => string | undefined);
cellClass?: string | (() => string | undefined);
ariaSort?: () => 'ascending' | 'descending' | 'none';
};
</script>
<script lang="ts" generics="Row, Column extends TableColumn">
import * as UITable from '$lib/components/ui/table';
import { cn } from '$lib/utils.js';
type Props = {
title?: string;
data: Row[];
columns: Column[];
getRowKey: (row: Row, index: number) => string | number;
rowLabel?: (row: Row, index: number) => string;
rowClass?: string | ((row: Row, index: number) => string);
containerClass?: string;
tableClass?: string;
headerClass?: string;
onrowclick?: (row: Row, index: number) => void;
header?: Snippet<[Column]>;
cell: Snippet<[Row, Column, number]>;
};
let {
title,
data,
columns,
getRowKey,
rowLabel,
rowClass,
containerClass = 'mt-4 overflow-hidden rounded-2xl border bg-card',
tableClass = 'min-w-[980px]',
headerClass = 'bg-muted/50 hover:bg-muted/50 [&_th]:text-muted-foreground',
onrowclick,
header,
cell
}: Props = $props();
function labelText(label: string | (() => string)) {
return typeof label === 'function' ? label() : label;
}
function resolvedRowClass(row: Row, index: number) {
return typeof rowClass === 'function' ? rowClass(row, index) : rowClass;
}
function resolvedColumnClass(className: string | (() => string | undefined) | undefined) {
return typeof className === 'function' ? className() : className;
}
function handleRowKeydown(event: KeyboardEvent, row: Row, index: number) {
if (!onrowclick || (event.key !== 'Enter' && event.key !== ' ')) return;
event.preventDefault();
onrowclick(row, index);
}
</script>
<section class={containerClass}>
{#if title}
<h2 class="sr-only">{title}</h2>
{/if}
<UITable.Root class={tableClass}>
<UITable.Header>
<UITable.Row class={headerClass}>
{#each columns as column (column.key)}
<UITable.Head
class={cn('text-muted-foreground', resolvedColumnClass(column.headClass))}
aria-sort={column.ariaSort?.() ?? 'none'}
>
{#if header}
{@render header(column)}
{:else}
{labelText(column.label)}
{/if}
</UITable.Head>
{/each}
</UITable.Row>
</UITable.Header>
<UITable.Body>
{#each data as row, index (getRowKey(row, index))}
<UITable.Row
class={resolvedRowClass(row, index)}
role={onrowclick ? 'button' : undefined}
tabindex={onrowclick ? 0 : undefined}
aria-label={rowLabel?.(row, index)}
onclick={() => onrowclick?.(row, index)}
onkeydown={(event) => handleRowKeydown(event, row, index)}
>
{#each columns as column (column.key)}
<UITable.Cell class={resolvedColumnClass(column.cellClass)}>
{@render cell(row, column, index)}
</UITable.Cell>
{/each}
</UITable.Row>
{/each}
</UITable.Body>
</UITable.Root>
</section>