beeeeg update
Some checks failed
Test and deploy staging / staging (push) Has been cancelled

This commit is contained in:
2026-09-23 04:15:10 +00:00
parent 487eff5bb2
commit d0c7b6548b
135 changed files with 9531 additions and 1777 deletions

View File

@@ -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}