Files
website/src/routes/blog/+page.svelte
Stan Runge c2ba8febb4
Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s
Expand website and game trackers
2026-09-25 22:43:04 +00:00

114 lines
4.1 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
let hydrated = $state(false);
onMount(() => {
hydrated = true;
});
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { enhance } from '$app/forms';
import { resolve } from '$app/paths';
import { calendarDate, relativeTime } from '$lib/relative-time';
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import type { PageProps } from './$types';
let { data, form }: PageProps = $props();
let searchQuery = $state('');
let selectedTag = $state('');
let tags = $derived([...new Set(data.blogArticles.flatMap((article) => article.tags))].sort());
let visibleArticles = $derived(
data.blogArticles.filter((article) => {
const query = searchQuery.trim().toLowerCase();
return (
(!selectedTag || article.tags.includes(selectedTag)) &&
(!query ||
`${article.title} ${article.content} ${article.tags.join(' ')}`
.toLowerCase()
.includes(query))
);
})
);
</script>
<div class="mx-auto max-w-6xl px-4 py-12 sm:px-6 sm:py-16">
<div class="mb-10 flex flex-wrap items-center justify-between gap-4">
<h1 class="text-4xl font-semibold tracking-tight sm:text-5xl">blog</h1>
{#if data.isAdmin}<Dialog.Root>
<Dialog.Trigger disabled={!hydrated} class="underline underline-offset-4"
>new article</Dialog.Trigger
>
<Dialog.Content
><Dialog.Header
><Dialog.Title>new article</Dialog.Title><Dialog.Description
>Give your draft a title, then continue writing.</Dialog.Description
></Dialog.Header
>
{#if form?.message}<p role="alert" class="text-destructive">{form.message}</p>{/if}
<form method="POST" use:enhance class="space-y-4">
<label class="block"
>title<Input name="title" required maxlength={256} class="mt-2" /></label
>
<Button type="submit">create draft</Button>
</form>
</Dialog.Content></Dialog.Root
>{/if}
</div>
{#if form?.message}<p role="alert" class="mb-5 text-red-500">{form.message}</p>{/if}
<div class="mb-8 space-y-4">
<label class="block max-w-lg">
<span class="sr-only">Search articles</span>
<Input type="search" placeholder="search articles" bind:value={searchQuery} class="h-11" />
</label>
{#if tags.length}
<div class="flex flex-wrap gap-x-4 gap-y-2" aria-label="Filter by tag">
<Button
variant={!selectedTag ? 'secondary' : 'ghost'}
size="sm"
type="button"
onclick={() => (selectedTag = '')}
aria-pressed={!selectedTag}>all</Button
>
{#each tags as tag (tag)}
<Button
variant={selectedTag === tag ? 'secondary' : 'ghost'}
size="sm"
type="button"
onclick={() => (selectedTag = selectedTag === tag ? '' : tag)}
aria-pressed={selectedTag === tag}>{tag}</Button
>
{/each}
</div>
{/if}
</div>
<ul>
{#each visibleArticles as article (article.id)}
<li class="border-t border-[var(--site-border)] py-5">
<div class="flex flex-wrap items-baseline justify-between gap-x-6 gap-y-1">
<a
href={resolve('/blog/[slug]', { slug: article.slug })}
class="text-lg font-medium hover:text-[var(--site-muted)]">{article.title}</a
>
<div
class="flex flex-wrap items-center gap-x-4 gap-y-1 text-base text-[var(--site-muted)]"
>
{#if data.isAdmin && article.status === 'draft'}<Badge variant="outline">draft</Badge
>{/if}
{#each article.tags as tag (tag)}<Badge variant="secondary">{tag}</Badge>{/each}
<time datetime={article.createdAt.toISOString()} title={calendarDate(article.createdAt)}
>{relativeTime(article.createdAt, data.now)}</time
>
{#if data.isAdmin}<a
href={resolve('/blog/[slug]/edit', { slug: article.slug })}
class="hover:text-[var(--site-text)]">edit</a
>{/if}
</div>
</div>
</li>
{:else}
<li class="border-t border-[var(--site-border)] py-5 text-[var(--site-muted)]">
{data.blogArticles.length ? 'no matching articles' : 'no articles yet'}
</li>
{/each}
</ul>
</div>