Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s
114 lines
4.1 KiB
Svelte
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>
|