refactor:
This commit is contained in:
parent
38b5413a37
commit
7f01bec7e6
17 changed files with 121 additions and 132 deletions
|
|
@ -1,3 +1,29 @@
|
||||||
|
import { getSession } from 'auth-astro/server';
|
||||||
|
|
||||||
|
type Session = { user?: { id?: string; name?: string | null } };
|
||||||
|
|
||||||
export function isAdmin(userId: string | undefined): boolean {
|
export function isAdmin(userId: string | undefined): boolean {
|
||||||
return userId === import.meta.env.ADMIN_GITHUB_ID;
|
return userId === import.meta.env.ADMIN_GITHUB_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function requireAdminSession(request: Request): Promise<
|
||||||
|
| { session: Session; error: null }
|
||||||
|
| { session: null; error: Response | null }
|
||||||
|
> {
|
||||||
|
let session: Session | null;
|
||||||
|
try {
|
||||||
|
session = await getSession(request);
|
||||||
|
} catch {
|
||||||
|
return { session: null, error: new Response('Auth not configured', { status: 500 }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return { session: null, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin(session.user?.id)) {
|
||||||
|
return { session: null, error: new Response('Forbidden', { status: 403 }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { session, error: null };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,27 @@ export function extractDomain(url: string): string {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatListItem(
|
||||||
|
date: Date,
|
||||||
|
url: string,
|
||||||
|
title: string,
|
||||||
|
options?: { pinned?: boolean; suffix?: string }
|
||||||
|
): string {
|
||||||
|
const pinnedBadge = options?.pinned ? ' [pinned]' : '';
|
||||||
|
const suffix = options?.suffix ? ` ${options.suffix}` : '';
|
||||||
|
return `<span class="muted">${formatDate(date)}</span> <a href="${url}">${title}</a>${pinnedBadge}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Sortable {
|
||||||
|
date: Date;
|
||||||
|
pinned?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortByPinnedThenDate<T extends Sortable>(items: T[]): T[] {
|
||||||
|
return items.slice().sort((a, b) => {
|
||||||
|
if (a.pinned && !b.pinned) return -1;
|
||||||
|
if (!a.pinned && b.pinned) return 1;
|
||||||
|
return b.date.getTime() - a.date.getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export function getSlug(postId: string): string {
|
||||||
return parts[parts.length - 1];
|
return parts[parts.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortPosts(posts: Post[]): Post[] {
|
function sortPosts(posts: Post[]): Post[] {
|
||||||
return posts.slice().sort((a, b) => {
|
return posts.slice().sort((a, b) => {
|
||||||
if (a.data.pinned && !b.data.pinned) return -1;
|
if (a.data.pinned && !b.data.pinned) return -1;
|
||||||
if (!a.data.pinned && b.data.pinned) return 1;
|
if (!a.data.pinned && b.data.pinned) return 1;
|
||||||
|
|
@ -2,6 +2,7 @@ import { execSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
|
import { sortByPinnedThenDate } from './format';
|
||||||
|
|
||||||
function getGitDate(filePath: string): Date {
|
function getGitDate(filePath: string): Date {
|
||||||
try {
|
try {
|
||||||
|
|
@ -40,18 +41,14 @@ export function getTxtFiles(): TxtFile[] {
|
||||||
const config = loadTxtConfig();
|
const config = loadTxtConfig();
|
||||||
const pinnedSet = new Set(config.pinned || []);
|
const pinnedSet = new Set(config.pinned || []);
|
||||||
|
|
||||||
return fs.readdirSync(txtDir)
|
const files = fs.readdirSync(txtDir)
|
||||||
.filter(file => file.endsWith('.txt'))
|
.filter(file => file.endsWith('.txt'))
|
||||||
.map(name => ({
|
.map(name => ({
|
||||||
name,
|
name,
|
||||||
date: getGitDate(path.join(txtDir, name)),
|
date: getGitDate(path.join(txtDir, name)),
|
||||||
pinned: pinnedSet.has(name),
|
pinned: pinnedSet.has(name),
|
||||||
}))
|
}));
|
||||||
.sort((a, b) => {
|
return sortByPinnedThenDate(files);
|
||||||
if (a.pinned && !b.pinned) return -1;
|
|
||||||
if (!a.pinned && b.pinned) return 1;
|
|
||||||
return b.date.getTime() - a.date.getTime();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTxtFileNames(): string[] {
|
export function getTxtFileNames(): string[] {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,14 @@
|
||||||
---
|
---
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
import { getSession } from 'auth-astro/server';
|
|
||||||
import { getPendingEntries, type GuestbookEntry } from '../lib/db';
|
import { getPendingEntries, type GuestbookEntry } from '../lib/db';
|
||||||
import { isAdmin } from '../lib/auth';
|
import { requireAdminSession } from '../lib/auth';
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import { formatDate } from '../lib/format';
|
import { formatDate } from '../lib/format';
|
||||||
|
|
||||||
let session;
|
const { session, error } = await requireAdminSession(Astro.request);
|
||||||
try {
|
if (error) return error;
|
||||||
session = await getSession(Astro.request);
|
if (!session) return Astro.redirect('/api/auth/signin');
|
||||||
} catch {
|
|
||||||
return new Response('Auth not configured', { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return Astro.redirect('/api/auth/signin');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAdmin(session.user?.id)) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries: GuestbookEntry[] = [];
|
let entries: GuestbookEntry[] = [];
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import { formatDate, extractDomain } from '../../lib/format';
|
import { formatListItem, extractDomain } from '../../lib/format';
|
||||||
|
|
||||||
const bookmarksCollection = await getCollection('bookmarks');
|
const bookmarksCollection = await getCollection('bookmarks');
|
||||||
const bookmarks = bookmarksCollection
|
const bookmarks = bookmarksCollection
|
||||||
|
|
@ -11,6 +11,6 @@ const bookmarks = bookmarksCollection
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary>bookmarks</summary>
|
<summary>bookmarks</summary>
|
||||||
<pre set:html={bookmarks.map(b => `<span class="muted">${formatDate(b.data.date)}</span> <a href="${b.data.url}">${b.data.title}</a> <span class="muted">(${extractDomain(b.data.url)})</span>`).join('\n')} />
|
<pre set:html={bookmarks.map(b => formatListItem(b.data.date, b.data.url, b.data.title, { suffix: `<span class="muted">(${extractDomain(b.data.url)})</span>` })).join('\n')} />
|
||||||
</details>
|
</details>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,15 @@
|
||||||
---
|
---
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
import { getSession } from 'auth-astro/server';
|
|
||||||
import { getCollection, render } from 'astro:content';
|
import { getCollection, render } from 'astro:content';
|
||||||
import { isAdmin } from '../../lib/auth';
|
import { requireAdminSession } from '../../lib/auth';
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import { formatDate } from '../../lib/format';
|
import { formatDate } from '../../lib/format';
|
||||||
import { getSlug } from '../../lib/posts';
|
import { getSlug } from '../../lib/md';
|
||||||
|
|
||||||
let session;
|
const { session, error } = await requireAdminSession(Astro.request);
|
||||||
try {
|
if (error) return error;
|
||||||
session = await getSession(Astro.request);
|
if (!session) return Astro.redirect('/api/auth/signin');
|
||||||
} catch {
|
|
||||||
return new Response('Auth not configured', { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return Astro.redirect('/api/auth/signin');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAdmin(session.user?.id)) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const slug = Astro.params.slug;
|
const slug = Astro.params.slug;
|
||||||
const posts = await getCollection('md', ({ data }) => data.draft === true);
|
const posts = await getCollection('md', ({ data }) => data.draft === true);
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,15 @@
|
||||||
---
|
---
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
import { getSession } from 'auth-astro/server';
|
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import { isAdmin } from '../../lib/auth';
|
import { requireAdminSession } from '../../lib/auth';
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import { formatDate } from '../../lib/format';
|
import { formatListItem } from '../../lib/format';
|
||||||
import { organizePostsByCategory, getSlug } from '../../lib/posts';
|
import { organizePostsByCategory, getSlug } from '../../lib/md';
|
||||||
|
|
||||||
let session;
|
const { session, error } = await requireAdminSession(Astro.request);
|
||||||
try {
|
if (error) return error;
|
||||||
session = await getSession(Astro.request);
|
if (!session) return Astro.redirect('/api/auth/signin');
|
||||||
} catch {
|
|
||||||
return new Response('Auth not configured', { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return Astro.redirect('/api/auth/signin');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAdmin(session.user?.id)) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const posts = await getCollection('md', ({ data }) => data.draft === true);
|
const posts = await getCollection('md', ({ data }) => data.draft === true);
|
||||||
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
|
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
|
||||||
|
|
@ -36,7 +24,7 @@ const { grouped, categories: sortedCategories } = organizePostsByCategory(posts)
|
||||||
sortedCategories.map(category => (
|
sortedCategories.map(category => (
|
||||||
<details open>
|
<details open>
|
||||||
<summary>{category}</summary>
|
<summary>{category}</summary>
|
||||||
<pre set:html={grouped[category].map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/draft/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
|
<pre set:html={grouped[category].map(post => formatListItem(post.data.date, `/draft/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })).join('\n')} />
|
||||||
</details>
|
</details>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import rss from '@astrojs/rss';
|
import rss from '@astrojs/rss';
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import type { APIContext } from 'astro';
|
import type { APIContext } from 'astro';
|
||||||
import { getSlug } from '../lib/posts';
|
import { getSlug } from '../lib/md';
|
||||||
import { getTxtFiles } from '../lib/txt';
|
import { getTxtFiles } from '../lib/txt';
|
||||||
|
|
||||||
export async function GET(context: APIContext) {
|
export async function GET(context: APIContext) {
|
||||||
|
|
|
||||||
|
|
@ -29,33 +29,7 @@ try {
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('sign-guestbook')?.addEventListener('click', async (e) => {
|
import { initGuestbookSign } from '../../scripts/guestbook-sign';
|
||||||
e.preventDefault();
|
initGuestbookSign();
|
||||||
const status = document.getElementById('guestbook-status')!;
|
|
||||||
|
|
||||||
const name = prompt('name:');
|
|
||||||
if (!name) return;
|
|
||||||
|
|
||||||
const message = prompt('message:');
|
|
||||||
if (!message) return;
|
|
||||||
|
|
||||||
const url = prompt('url (optional):');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/guestbook', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name, message, url: url || null }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
status.textContent = ' thanks! pending approval.';
|
|
||||||
} else {
|
|
||||||
status.textContent = ' error';
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
status.textContent = ' failed';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import { getApprovedEntries, type GuestbookEntry } from '../lib/db';
|
import { getApprovedEntries, type GuestbookEntry } from '../lib/db';
|
||||||
import { formatDate, extractDomain } from '../lib/format';
|
import { formatDate, extractDomain, formatListItem } from '../lib/format';
|
||||||
import { organizePostsByCategory, getSlug } from '../lib/posts';
|
import { organizePostsByCategory, getSlug } from '../lib/md';
|
||||||
import { getTxtFiles } from '../lib/txt';
|
import { getTxtFiles } from '../lib/txt';
|
||||||
|
|
||||||
const posts = await getCollection('md', ({ data }) => data.draft !== true);
|
const posts = await getCollection('md', ({ data }) => data.draft !== true);
|
||||||
|
|
@ -30,7 +30,7 @@ try {
|
||||||
<details open>
|
<details open>
|
||||||
<summary>{category}</summary>
|
<summary>{category}</summary>
|
||||||
<pre set:html={[
|
<pre set:html={[
|
||||||
...categoryPosts.slice(0, 10).map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/md/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`),
|
...categoryPosts.slice(0, 10).map(post => formatListItem(post.data.date, `/md/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })),
|
||||||
...(categoryPosts.length > 10 ? [`<a href="/md/">+${categoryPosts.length - 10} more</a>`] : [])
|
...(categoryPosts.length > 10 ? [`<a href="/md/">+${categoryPosts.length - 10} more</a>`] : [])
|
||||||
].join('\n')} />
|
].join('\n')} />
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -40,7 +40,7 @@ try {
|
||||||
<details open>
|
<details open>
|
||||||
<summary>txt</summary>
|
<summary>txt</summary>
|
||||||
<pre set:html={[
|
<pre set:html={[
|
||||||
...txtFiles.slice(0, 10).map(f => `<span class="muted">${formatDate(f.date)}</span> <a href="/txt/${f.name}">${f.name}</a>${f.pinned ? ' [pinned]' : ''}`),
|
...txtFiles.slice(0, 10).map(f => formatListItem(f.date, `/txt/${f.name}`, f.name, { pinned: f.pinned })),
|
||||||
...(txtFiles.length > 10 ? [`<a href="/txt/">+${txtFiles.length - 10} more</a>`] : [])
|
...(txtFiles.length > 10 ? [`<a href="/txt/">+${txtFiles.length - 10} more</a>`] : [])
|
||||||
].join('\n')} />
|
].join('\n')} />
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -48,7 +48,7 @@ try {
|
||||||
<details open>
|
<details open>
|
||||||
<summary>bookmarks</summary>
|
<summary>bookmarks</summary>
|
||||||
<pre set:html={[
|
<pre set:html={[
|
||||||
...bookmarks.slice(0, 10).map(b => `<span class="muted">${formatDate(b.data.date)}</span> <a href="${b.data.url}">${b.data.title}</a> <span class="muted">(${extractDomain(b.data.url)})</span>`),
|
...bookmarks.slice(0, 10).map(b => formatListItem(b.data.date, b.data.url, b.data.title, { suffix: `<span class="muted">(${extractDomain(b.data.url)})</span>` })),
|
||||||
...(bookmarks.length > 10 ? [`<a href="/bookmarks/">+${bookmarks.length - 10} more</a>`] : [])
|
...(bookmarks.length > 10 ? [`<a href="/bookmarks/">+${bookmarks.length - 10} more</a>`] : [])
|
||||||
].join('\n')} />
|
].join('\n')} />
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -70,33 +70,7 @@ try {
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('sign-guestbook')?.addEventListener('click', async (e) => {
|
import { initGuestbookSign } from '../scripts/guestbook-sign';
|
||||||
e.preventDefault();
|
initGuestbookSign();
|
||||||
const status = document.getElementById('guestbook-status')!;
|
|
||||||
|
|
||||||
const name = prompt('name:');
|
|
||||||
if (!name) return;
|
|
||||||
|
|
||||||
const message = prompt('message:');
|
|
||||||
if (!message) return;
|
|
||||||
|
|
||||||
const url = prompt('url (optional):');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/guestbook', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name, message, url: url || null }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
status.textContent = ' thanks! pending approval.';
|
|
||||||
} else {
|
|
||||||
status.textContent = ' error';
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
status.textContent = ' failed';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { getCollection, render } from 'astro:content';
|
import { getCollection, render } from 'astro:content';
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import { formatDate } from '../../lib/format';
|
import { formatDate } from '../../lib/format';
|
||||||
import { getSlug } from '../../lib/posts';
|
import { getSlug } from '../../lib/md';
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection('md', ({ data }) => data.draft !== true);
|
const posts = await getCollection('md', ({ data }) => data.draft !== true);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import { formatDate } from '../../lib/format';
|
import { formatListItem } from '../../lib/format';
|
||||||
import { organizePostsByCategory, getSlug } from '../../lib/posts';
|
import { organizePostsByCategory, getSlug } from '../../lib/md';
|
||||||
|
|
||||||
const posts = await getCollection('md', ({ data }) => data.draft !== true);
|
const posts = await getCollection('md', ({ data }) => data.draft !== true);
|
||||||
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
|
const { grouped, categories: sortedCategories } = organizePostsByCategory(posts);
|
||||||
|
|
@ -12,7 +12,7 @@ const { grouped, categories: sortedCategories } = organizePostsByCategory(posts)
|
||||||
{sortedCategories.map(category => (
|
{sortedCategories.map(category => (
|
||||||
<details open>
|
<details open>
|
||||||
<summary>{category}</summary>
|
<summary>{category}</summary>
|
||||||
<pre set:html={grouped[category].map(post => `<span class="muted">${formatDate(post.data.date)}</span> <a href="/md/${getSlug(post.id)}">${post.data.title}</a>${post.data.pinned ? ' [pinned]' : ''}`).join('\n')} />
|
<pre set:html={grouped[category].map(post => formatListItem(post.data.date, `/md/${getSlug(post.id)}`, post.data.title, { pinned: post.data.pinned })).join('\n')} />
|
||||||
</details>
|
</details>
|
||||||
))}
|
))}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import type { APIContext } from 'astro';
|
import type { APIContext } from 'astro';
|
||||||
import { getSlug } from '../lib/posts';
|
import { getSlug } from '../lib/md';
|
||||||
import { getTxtFileNames } from '../lib/txt';
|
import { getTxtFileNames } from '../lib/txt';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import type { APIContext } from 'astro';
|
import type { APIContext } from 'astro';
|
||||||
import { getSlug } from '../lib/posts';
|
import { getSlug } from '../lib/md';
|
||||||
import { getTxtFileNames } from '../lib/txt';
|
import { getTxtFileNames } from '../lib/txt';
|
||||||
|
|
||||||
const SUBDOMAINS = [
|
const SUBDOMAINS = [
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import { formatDate } from '../../lib/format';
|
import { formatListItem } from '../../lib/format';
|
||||||
import { getTxtFiles } from '../../lib/txt';
|
import { getTxtFiles } from '../../lib/txt';
|
||||||
|
|
||||||
const txtFiles = getTxtFiles();
|
const txtFiles = getTxtFiles();
|
||||||
|
|
@ -9,6 +9,6 @@ const txtFiles = getTxtFiles();
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary>txt</summary>
|
<summary>txt</summary>
|
||||||
<pre set:html={txtFiles.map(f => `<span class="muted">${formatDate(f.date)}</span> <a href="/txt/${f.name}">${f.name}</a>${f.pinned ? ' [pinned]' : ''}`).join('\n')} />
|
<pre set:html={txtFiles.map(f => formatListItem(f.date, `/txt/${f.name}`, f.name, { pinned: f.pinned })).join('\n')} />
|
||||||
</details>
|
</details>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
30
www/src/scripts/guestbook-sign.ts
Normal file
30
www/src/scripts/guestbook-sign.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
export function initGuestbookSign() {
|
||||||
|
document.getElementById('sign-guestbook')?.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const status = document.getElementById('guestbook-status')!;
|
||||||
|
|
||||||
|
const name = prompt('name:');
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
const message = prompt('message:');
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
const url = prompt('url (optional):');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/guestbook', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, message, url: url || null }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
status.textContent = ' thanks! pending approval.';
|
||||||
|
} else {
|
||||||
|
status.textContent = ' error';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
status.textContent = ' failed';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue