126 lines
3.6 KiB
Svelte
126 lines
3.6 KiB
Svelte
<script lang="ts" generics="TData, TValue">
|
|
import { onMount } from 'svelte';
|
|
import {
|
|
type ColumnDef,
|
|
getCoreRowModel
|
|
} from "@tanstack/table-core";
|
|
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
|
import * as Table from "$lib/components/ui/table/index.js";
|
|
|
|
type DataTableProps<TData, TValue> = {
|
|
columns: ColumnDef<TData, TValue>[];
|
|
data: TData[];
|
|
loading: boolean;
|
|
hasMore: boolean;
|
|
loadMore: () => void;
|
|
};
|
|
|
|
let {
|
|
data,
|
|
columns,
|
|
loading,
|
|
hasMore,
|
|
loadMore
|
|
}: DataTableProps<TData, TValue> = $props();
|
|
|
|
const table = createSvelteTable({
|
|
get data() {
|
|
return data;
|
|
},
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel()
|
|
});
|
|
|
|
let scrollContainer = $state<HTMLDivElement>();
|
|
let loadingTrigger = $state<HTMLDivElement>();
|
|
|
|
// Intersection Observer para detectar cuando el usuario llega al final
|
|
onMount(() => {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
const [entry] = entries;
|
|
if (entry.isIntersecting && hasMore && !loading) {
|
|
loadMore();
|
|
}
|
|
},
|
|
{
|
|
root: scrollContainer,
|
|
threshold: 0.1
|
|
}
|
|
);
|
|
|
|
if (loadingTrigger) {
|
|
observer.observe(loadingTrigger);
|
|
}
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<div class="flex flex-col h-full w-full overflow-hidden">
|
|
<div class="relative min-h-[320px] max-h-[calc(100svh-340px)] flex-1 overflow-auto bg-card shadow-inner" bind:this={scrollContainer}>
|
|
<table class="w-full caption-bottom text-sm border-collapse">
|
|
<thead class="sticky top-0 z-20 bg-background/95 shadow-sm backdrop-blur-md border-b">
|
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
|
<tr>
|
|
{#each headerGroup.headers as header (header.id)}
|
|
<th class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
|
|
{#if !header.isPlaceholder}
|
|
<FlexRender
|
|
content={header.column.columnDef.header}
|
|
context={header.getContext()}
|
|
/>
|
|
{/if}
|
|
</th>
|
|
{/each}
|
|
</tr>
|
|
{/each}
|
|
</thead>
|
|
<tbody class="[&_tr:last-child]:border-0">
|
|
{#each table.getRowModel().rows as row (row.id)}
|
|
<tr class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
|
|
{#each row.getVisibleCells() as cell (cell.id)}
|
|
<td class="p-4 align-middle [&:has([role=checkbox])]:pr-0 whitespace-nowrap">
|
|
<FlexRender
|
|
content={cell.column.columnDef.cell}
|
|
context={cell.getContext()}
|
|
/>
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
{:else}
|
|
<tr>
|
|
<td colspan={columns.length} class="h-24 text-center">
|
|
No hay resultados.
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
|
|
<!-- Loading Trigger -->
|
|
{#if hasMore}
|
|
<tr>
|
|
<td colspan={columns.length} class="h-24 text-center p-0">
|
|
<div bind:this={loadingTrigger} class="flex items-center justify-center h-full w-full">
|
|
{#if loading}
|
|
<div class="flex items-center justify-center gap-3 bg-muted/30 px-6 py-2 rounded-full border shadow-sm">
|
|
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent text-primary"></div>
|
|
<span class="text-foreground font-medium text-sm">Cargando más registros...</span>
|
|
</div>
|
|
{:else}
|
|
<div class="text-muted-foreground text-sm flex items-center gap-2">
|
|
<span class="h-px w-8 bg-border"></span>
|
|
<span>Desplázate para cargar más</span>
|
|
<span class="h-px w-8 bg-border"></span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{/if}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|