126 lines
3.8 KiB
Svelte
126 lines
3.8 KiB
Svelte
<script lang="ts">
|
|
import { fade } from 'svelte/transition';
|
|
import { cubicInOut } from 'svelte/easing';
|
|
import { fetchEntries, formatter } from '../util';
|
|
|
|
let entriesPromise = $state(fetchEntries());
|
|
|
|
let interval: Parameters<typeof clearInterval>[0] = undefined;
|
|
|
|
let y = $state(0);
|
|
let scrolled = $derived(y > 32);
|
|
|
|
$effect(() => {
|
|
clearInterval(interval);
|
|
|
|
// Pooling interval to fetch entries every 31 seconds
|
|
let fetching = false;
|
|
interval = setInterval(async () => {
|
|
if (fetching) return;
|
|
fetching = true;
|
|
try {
|
|
const promise = fetchEntries();
|
|
// wait for the promise to resolve before assigning to prevent flash
|
|
await promise;
|
|
entriesPromise = promise;
|
|
} catch (error) {
|
|
console.error('Error fetching entries:', error);
|
|
} finally {
|
|
fetching = false;
|
|
}
|
|
}, 1000 * 31);
|
|
|
|
return () => clearInterval(interval);
|
|
});
|
|
</script>
|
|
|
|
<svelte:window bind:scrollY={y} />
|
|
|
|
<div class="relative col-[content] pr-0.5">
|
|
{#await entriesPromise}
|
|
<p class="text-center">Loading entries...</p>
|
|
{:then entries}
|
|
{#if entries.length === 0}
|
|
<p class="text-center">No entries.</p>
|
|
{:else}
|
|
{@render table(entries)}
|
|
{/if}
|
|
{:catch error}
|
|
<p class="mx-auto max-w-md text-center">
|
|
Something went wrong fetching entries: {error.message}
|
|
</p>
|
|
{/await}
|
|
</div>
|
|
|
|
{#snippet table(entries: Awaited<ReturnType<typeof fetchEntries>>)}
|
|
<div
|
|
class="@container grid w-full grid-cols-[auto,1fr,auto,auto,auto] gap-1 font-light @lg:gap-3"
|
|
>
|
|
<div
|
|
class="sticky top-(--header-height) col-span-5 grid grid-cols-[subgrid] gap-1 bg-gray-950 py-2 text-gray-400 capitalize"
|
|
>
|
|
<div>id</div>
|
|
<div class="px-1">name</div>
|
|
<div class="hidden px-1 @3xl:block">done</div>
|
|
<div class="hidden px-1 @5xl:block">created</div>
|
|
<div class="hidden px-1 @xl:block">updated</div>
|
|
|
|
{#if scrolled}
|
|
<div
|
|
transition:fade={{ duration: 250, easing: cubicInOut }}
|
|
class="absolute inset-x-0 top-full h-16 bg-gradient-to-b from-gray-950 to-transparent"
|
|
></div>
|
|
{/if}
|
|
</div>
|
|
{#each entries as entry (entry.id)}
|
|
<div class="col-span-5 grid grid-cols-[subgrid] gap-1">
|
|
<div class="py-2 text-xs text-gray-400/50">{entry.id}</div>
|
|
<a
|
|
href={entry.href}
|
|
class="line-clamp-1 block px-2 py-1 text-nowrap text-ellipsis text-gray-200 capitalize transition-colors hover:text-pink-600 hover:underline"
|
|
target="_blank"
|
|
title={entry.name}
|
|
referrerpolicy="no-referrer">{entry.name}</a
|
|
>
|
|
{@render done(entry.finished)}
|
|
<div class="hidden max-w-24 px-1 py-1 text-xs text-gray-200 @5xl:block">
|
|
{formatter.format(entry.created_at)}
|
|
</div>
|
|
<div class="hidden max-w-24 px-1 py-1 text-xs text-gray-200 @xl:block">
|
|
{formatter.format(entry.updated_at)}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
<div class="sticky inset-x-0 bottom-0 h-16 bg-gradient-to-t from-gray-950 to-transparent"></div>
|
|
{/snippet}
|
|
|
|
{#snippet done(finished: boolean)}
|
|
{#if finished}
|
|
<svg
|
|
class="hidden text-green-500 @3xl:block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg
|
|
>
|
|
{:else}
|
|
<svg
|
|
class="hidden text-red-500 @3xl:block"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg
|
|
>
|
|
{/if}
|
|
{/snippet}
|