This commit is contained in:
@@ -1,125 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let createNewArticlePopup = $state(false);
|
||||
let loginPopup = $state(false);
|
||||
let password = $state('');
|
||||
let loginError = $state('');
|
||||
let loginLoading = $state(false);
|
||||
|
||||
async function login() {
|
||||
loginLoading = true;
|
||||
loginError = '';
|
||||
const res = await fetch('/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
if (res.ok) {
|
||||
loginPopup = false;
|
||||
password = '';
|
||||
await invalidateAll();
|
||||
} else {
|
||||
loginError = 'Wrong password';
|
||||
}
|
||||
loginLoading = false;
|
||||
}
|
||||
import { calendarDate, relativeTime } from '$lib/relative-time';
|
||||
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="text-center py-8 gap-2 flex align-center justify-center">
|
||||
<h1 class="text-2xl">Blog (Stan's yapping corner)</h1>
|
||||
{#if data.isAdmin}
|
||||
<button
|
||||
onclick={() => (createNewArticlePopup = true)}
|
||||
class="bg-gray-800 rounded px-4 py-2 text-white font-bold cursor-pointer"
|
||||
>Write new article</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => (loginPopup = true)}
|
||||
class="text-xs text-gray-400 underline cursor-pointer">Admin login</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
{#each data.blogArticles as article (article.slug)}
|
||||
<a href={resolve('/blog/[slug]', { slug: article.slug })}>
|
||||
<div class="flex gap-2 py-2 px-4 border rounded min-w-100">
|
||||
<p class="font-bold text-md">{article.title}</p>
|
||||
{#if data.isAdmin && article.status === 'draft'}
|
||||
<span class="text-xs uppercase text-orange-500">draft</span>
|
||||
<a href={resolve('/blog/[slug]/edit', { slug: article.slug })} class="text-xs underline"
|
||||
>edit</a
|
||||
<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}<details class="relative">
|
||||
<summary class="cursor-pointer text-base underline underline-offset-4">new article</summary>
|
||||
<form
|
||||
method="POST"
|
||||
use:enhance
|
||||
class="absolute top-full right-0 z-10 mt-3 w-72 max-w-[calc(100vw-2rem)] space-y-3 rounded-lg border border-[var(--site-border)] bg-[var(--site-surface)] p-4 shadow-lg"
|
||||
>
|
||||
<label class="block"
|
||||
>title<input
|
||||
name="title"
|
||||
required
|
||||
maxlength="256"
|
||||
class="mt-2 w-full rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
|
||||
/></label
|
||||
>
|
||||
{/if}
|
||||
<p>{article.createdAt?.toDateString()}</p>
|
||||
<button
|
||||
class="rounded bg-[var(--site-text)] px-4 py-2 font-semibold text-[var(--site-bg)]"
|
||||
>create draft</button
|
||||
>
|
||||
</form>
|
||||
</details>{/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="w-full rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-4 py-3 text-base"
|
||||
/>
|
||||
</label>
|
||||
{#if tags.length}
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-2" aria-label="Filter by tag">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (selectedTag = '')}
|
||||
aria-pressed={!selectedTag}
|
||||
class={selectedTag ? 'text-[var(--site-muted)]' : 'underline underline-offset-4'}
|
||||
>all</button
|
||||
>
|
||||
{#each tags as tag (tag)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (selectedTag = selectedTag === tag ? '' : tag)}
|
||||
aria-pressed={selectedTag === tag}
|
||||
class={selectedTag === tag
|
||||
? 'underline underline-offset-4'
|
||||
: 'text-[var(--site-muted)]'}>{tag}</button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/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'}<span>draft</span>{/if}
|
||||
{#each article.tags as tag (tag)}<span>{tag}</span>{/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>
|
||||
|
||||
{#if createNewArticlePopup}
|
||||
<div class="fixed inset-0 flex items-center justify-center bg-black/40">
|
||||
<form method="POST" use:enhance class="w-80 rounded bg-white p-6 shadow-xl">
|
||||
<h2 class="mb-4 text-lg font-bold">Create New Article</h2>
|
||||
<input
|
||||
name="title"
|
||||
placeholder="Article title"
|
||||
required
|
||||
class="mb-4 w-full rounded border border-gray-300 px-3 py-2 text-sm outline-none"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (createNewArticlePopup = false)}
|
||||
class="rounded px-3 py-2 text-sm cursor-pointer">Cancel</button
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-gray-800 px-3 py-2 text-sm font-bold text-white cursor-pointer"
|
||||
>Create draft</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loginPopup}
|
||||
<div class="fixed inset-0 flex items-center justify-center bg-black/40">
|
||||
<div class="w-80 rounded bg-white p-6 shadow-xl">
|
||||
<h2 class="mb-4 text-lg font-bold">Admin login</h2>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={password}
|
||||
placeholder="Password"
|
||||
onkeydown={(e) => e.key === 'Enter' && login()}
|
||||
class="mb-3 w-full rounded border border-gray-300 px-3 py-2 text-sm outline-none"
|
||||
/>
|
||||
{#if loginError}
|
||||
<p class="mb-3 text-xs text-red-500">{loginError}</p>
|
||||
{/if}
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
loginPopup = false;
|
||||
loginError = '';
|
||||
}}
|
||||
class="rounded px-3 py-2 text-sm cursor-pointer">Cancel</button
|
||||
>
|
||||
<button
|
||||
onclick={login}
|
||||
disabled={loginLoading}
|
||||
class="rounded bg-gray-800 px-3 py-2 text-sm font-bold text-white disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
||||
>
|
||||
{loginLoading ? 'Logging in…' : 'Log in'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user