Compare commits
4 Commits
79a1b42269
...
e384937596
Author | SHA1 | Date | |
---|---|---|---|
e384937596 | |||
3c3d596c9c | |||
c8cce26569 | |||
f12810a424 |
@ -1,8 +1,7 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { defineConfig, envField } from 'astro/config';
|
||||
import { modifiedTime } from './src/utils/lastModified.mjs';
|
||||
import mdx from "@astrojs/mdx";
|
||||
import db from "@astrojs/db";
|
||||
import node from "@astrojs/node";
|
||||
import devOnlyRoutes from '@fujocoded/astro-dev-only';
|
||||
|
||||
@ -15,7 +14,6 @@ export default defineConfig({
|
||||
},
|
||||
integrations: [
|
||||
mdx(),
|
||||
db(),
|
||||
devOnlyRoutes({
|
||||
// dryRun: true,
|
||||
routePatterns: ["/guestbook/admin"]
|
||||
@ -24,6 +22,11 @@ export default defineConfig({
|
||||
adapter: node({
|
||||
mode: "standalone",
|
||||
}),
|
||||
env: {
|
||||
schema: {
|
||||
TURSO_DATABASE_URL: envField.string({ context: "server", access: "secret" }),
|
||||
}
|
||||
},
|
||||
experimental: {
|
||||
fonts: [
|
||||
{
|
||||
|
18
db/config.ts
@ -1,18 +0,0 @@
|
||||
import { column, defineDb, defineTable, NOW } from 'astro:db';
|
||||
|
||||
const Guestbook = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
username: column.text(),
|
||||
website: column.text({ optional: true }),
|
||||
message: column.text({ multiline: true }),
|
||||
published: column.date({ default: NOW }),
|
||||
updated: column.date({ optional: true }),
|
||||
reply: column.text({ optional: true, multiline: true }),
|
||||
},
|
||||
});
|
||||
|
||||
// https://astro.build/db/config
|
||||
export default defineDb({
|
||||
tables: { Guestbook },
|
||||
});
|
8
db/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { TURSO_DATABASE_URL } from "astro:env/server";
|
||||
import { drizzle } from "drizzle-orm/libsql/web";
|
||||
import { createClient } from "@libsql/client";
|
||||
|
||||
const client = createClient({
|
||||
url: TURSO_DATABASE_URL,
|
||||
});
|
||||
export const db = drizzle({ client });
|
12
db/schema.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const guestbookTable = sqliteTable("guestbook_table", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
username: text().notNull(),
|
||||
website: text(),
|
||||
message: text().notNull(),
|
||||
published: text().notNull().default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updated: text().$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
|
||||
reply: text(),
|
||||
});
|
13
drizzle.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { loadEnv } from "vite";
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
const { TURSO_DATABASE_URL } = loadEnv(process.env.NODE_ENV!, process.cwd(), "");
|
||||
|
||||
export default defineConfig({
|
||||
out: "./db/migrations",
|
||||
schema: "./db/schema.ts",
|
||||
dialect: "turso",
|
||||
dbCredentials: {
|
||||
url: TURSO_DATABASE_URL,
|
||||
},
|
||||
});
|
BIN
guestbook.db
18
package.json
@ -9,18 +9,20 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/db": "0.17.1",
|
||||
"@astrojs/mdx": "^4.3.3",
|
||||
"@astrojs/node": "9.4.1",
|
||||
"@astrojs/mdx": "^4.3.6",
|
||||
"@astrojs/node": "9.4.4",
|
||||
"@astrojs/rss": "4.0.12",
|
||||
"@fujocoded/astro-dev-only": "0.0.4",
|
||||
"astro": "5.13.0",
|
||||
"@libsql/client": "^0.15.15",
|
||||
"astro": "5.13.10",
|
||||
"astro-breadcrumbs": "^3.3.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"isomorphic-dompurify": "^2.26.0",
|
||||
"marked": "^16.1.2"
|
||||
"dayjs": "^1.11.18",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"isomorphic-dompurify": "^2.28.0",
|
||||
"marked": "^16.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.17.1"
|
||||
"@types/node": "^22.18.6",
|
||||
"drizzle-kit": "^0.31.4"
|
||||
}
|
||||
}
|
||||
|
4834
pnpm-lock.yaml
generated
Normal file
5
pnpm-workspace.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- sharp
|
||||
|
||||
nodeLinker: hoisted
|
10
shippy.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export default {
|
||||
host: {
|
||||
host: "137.184.37.162",
|
||||
username: "haetae",
|
||||
remoteProjectPath: "/var/www/fujohost/haetae/",
|
||||
remoteDbPath: "/var/www/fujohost/haetae/guestbook.db",
|
||||
dbDriver: "astro:db",
|
||||
privateKeyPath: "/home/ayowaddup/.ssh/shippy.id_ed25519"
|
||||
}
|
||||
};
|
@ -1,22 +1,32 @@
|
||||
import { ActionError, defineAction } from "astro:actions";
|
||||
import { z } from "astro:content";
|
||||
import { db, eq, Guestbook, isDbError } from "astro:db";
|
||||
import { db } from "db";
|
||||
import { guestbookTable } from "db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
export const guestbook = {
|
||||
addEntry: defineAction({
|
||||
accept: "form",
|
||||
input: z.object({
|
||||
username: z.string().min(1, "You should have a name!"),
|
||||
username: z.string().nonempty("You should have a name!"),
|
||||
website: z.string().url().optional(),
|
||||
message: z.string().min(1, "Can't be that short..."),
|
||||
message: z.string().nonempty("Can't be that short..."),
|
||||
challenge: z.string().nonempty("Can't be empty!"),
|
||||
}),
|
||||
handler: async ({ username, website, message }) => {
|
||||
handler: async ({ username, website, message, challenge }) => {
|
||||
if (challenge !== "haetae") {
|
||||
throw new ActionError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Check the challenge question again!",
|
||||
});
|
||||
}
|
||||
|
||||
const addLine = message.replaceAll(/\r?\n/g, "<br />");
|
||||
const sanitized = DOMPurify.sanitize(addLine);
|
||||
|
||||
try {
|
||||
const entry = await db.insert(Guestbook).values({
|
||||
const entry = await db.insert(guestbookTable).values({
|
||||
username,
|
||||
website,
|
||||
message: sanitized,
|
||||
@ -24,10 +34,7 @@ export const guestbook = {
|
||||
|
||||
return entry[0];
|
||||
} catch (e) {
|
||||
if (isDbError(e)) {
|
||||
return new Response(`Cannot insert entry\n\n${e.message}`, { status: 400 });
|
||||
}
|
||||
return new Response('An unexpected error occurred', { status: 500 });
|
||||
return new Response(`An unexpected error occurred\n\n${e}`, { status: 500 });
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -43,7 +50,7 @@ export const guestbook = {
|
||||
throw new ActionError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const entry = await db.select().from(Guestbook).where(eq(Guestbook.id, id));
|
||||
const entry = await db.select().from(guestbookTable).where(eq(guestbookTable.id, id));
|
||||
if (!entry) {
|
||||
throw new ActionError({
|
||||
code: "NOT_FOUND",
|
||||
@ -55,17 +62,14 @@ export const guestbook = {
|
||||
const sanitized = DOMPurify.sanitize(addLine);
|
||||
|
||||
try {
|
||||
const update = await db.update(Guestbook).set({
|
||||
const update = await db.update(guestbookTable).set({
|
||||
reply: sanitized,
|
||||
updated: new Date(),
|
||||
}).where(eq(Guestbook.id, id)).returning();
|
||||
updated: new Date().toDateString(),
|
||||
}).where(eq(guestbookTable.id, id)).returning();
|
||||
|
||||
return update[0];
|
||||
} catch (e) {
|
||||
if (isDbError(e)) {
|
||||
return new Response(`Cannot update entry\n\n${e.message}`, { status: 400 });
|
||||
}
|
||||
return new Response('An unexpected error occurred', { status: 500 });
|
||||
return new Response(`An unexpected error occurred\n\n${e}`, { status: 500 });
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -79,7 +83,7 @@ export const guestbook = {
|
||||
throw new ActionError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const entry = await db.select().from(Guestbook).where(eq(Guestbook.id, id));
|
||||
const entry = await db.select().from(guestbookTable).where(eq(guestbookTable.id, id));
|
||||
if (!entry) {
|
||||
throw new ActionError({
|
||||
code: "NOT_FOUND",
|
||||
@ -88,14 +92,11 @@ export const guestbook = {
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await db.delete(Guestbook).where(eq(Guestbook.id, id)).returning();
|
||||
const entry = await db.delete(guestbookTable).where(eq(guestbookTable.id, id)).returning();
|
||||
|
||||
return entry[0];
|
||||
} catch (e) {
|
||||
if (isDbError(e)) {
|
||||
return new Response(`Cannot update entry\n\n${e.message}`, { status: 400 });
|
||||
}
|
||||
return new Response('An unexpected error occurred', { status: 500 });
|
||||
return new Response(`An unexpected error occurred\n\n${e}`, { status: 500 });
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
Before Width: | Height: | Size: 31 KiB |
BIN
src/assets/guild-bbs-bottom.png
Normal file
After Width: | Height: | Size: 483 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 875 B |
BIN
src/assets/guild-bbs-header.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 699 B After Width: | Height: | Size: 715 B |
@ -17,6 +17,7 @@ const { notes, lastModified }: Props = Astro.props;
|
||||
)}
|
||||
{notes && (
|
||||
<blockquote>
|
||||
<p class="author-note">Author's Notes:</p>
|
||||
<Fragment set:html={notes} />
|
||||
</blockquote>
|
||||
)}
|
||||
@ -39,12 +40,9 @@ const { notes, lastModified }: Props = Astro.props;
|
||||
margin: 1rem;
|
||||
font-size: calc(35px / 2);
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
content: "Author’s Notes:";
|
||||
font-weight: bold;
|
||||
.author-note {
|
||||
line-height: 1;
|
||||
alt: "Author's Notes:";
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
---
|
||||
import { db, desc, Guestbook } from "astro:db";
|
||||
import { desc } from "drizzle-orm";
|
||||
import { db } from "db";
|
||||
import { guestbookTable } from "db/schema";
|
||||
import formatDate from "@/utils/formatDate";
|
||||
import pikachu from "$/images/portrait-0025.png";
|
||||
|
||||
const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.published));
|
||||
const entries = await db.select().from(guestbookTable).orderBy(desc(guestbookTable.published));
|
||||
---
|
||||
<section id="entries">
|
||||
{entries.map(entry => (
|
||||
@ -16,7 +18,7 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
||||
? <a href={entry.website} target="_blank" referrerpolicy="no-referrer">{entry.username}</a>
|
||||
: <span>{entry.username}</span>}
|
||||
</h1>
|
||||
<time datetime={entry.published.toISOString()}>
|
||||
<time datetime={entry.published}>
|
||||
{formatDate(entry.published, false, 'MMMM D, YYYY')}
|
||||
</time>
|
||||
</header>
|
||||
@ -32,7 +34,7 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
||||
<div class="entry">
|
||||
<header>
|
||||
<h1>Reply to {entry.username}</h1>
|
||||
<time datetime={entry.updated?.toISOString()}>
|
||||
<time datetime={entry.updated}>
|
||||
{formatDate(entry.updated!, false, 'MMMM D, YYYY')}
|
||||
</time>
|
||||
</header>
|
||||
@ -44,6 +46,7 @@ const entries = await db.select().from(Guestbook).orderBy(desc(Guestbook.publish
|
||||
</article>}
|
||||
</>
|
||||
))}
|
||||
|
||||
{entries.length === 0 && (
|
||||
<article>
|
||||
<h1>Huh...</h1>
|
||||
|
@ -17,7 +17,7 @@ const links = [
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ThemeSwitch />
|
||||
<!-- <ThemeSwitch /> -->
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
@ -31,6 +31,7 @@ const links = [
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
7
src/components/Scrobbler.astro
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
const api_key = "";
|
||||
const username = "";
|
||||
const url = "";
|
||||
---
|
||||
|
||||
<span class="refresh" onclick="window.location.reload()">🗘</span>
|
@ -1,12 +0,0 @@
|
||||
<a id="top" href="#">
|
||||
back to top
|
||||
<!-- render this after scrolling -->
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#top {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
</style>
|
78
src/components/UpdateCard.astro
Normal file
@ -0,0 +1,78 @@
|
||||
---
|
||||
import formatDate from '@/utils/formatDate';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
date: Date | string;
|
||||
}
|
||||
|
||||
const { title, date } = Astro.props;
|
||||
---
|
||||
<article class="update card">
|
||||
<header>
|
||||
<h3><span>{title}</span></h3>
|
||||
<time datetime={formatDate(date, true)}>{formatDate(date, false, "MM/DD/YY")}</time>
|
||||
</header>
|
||||
|
||||
<div class="content inner">
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
/* UPDATE CARDS */
|
||||
.update {
|
||||
padding: 4px 2px;
|
||||
background-color: var(--bg-0);
|
||||
border-bottom: 2px solid var(--bg-4);
|
||||
|
||||
header {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
text-align: center;
|
||||
|
||||
& > * {
|
||||
font: normal 1.375rem var(--dotum-11-font);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
background-color: var(--bg-1);
|
||||
container: update-title / inline-size;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
time { background-color: var(--bg-3); }
|
||||
}
|
||||
|
||||
.inner {
|
||||
border: 2px solid var(--border-2);
|
||||
border-image: var(--innerBorder) 3 2 2 fill / 6px 4px 4px;
|
||||
padding: 4px;
|
||||
background-color: var(--bg-0);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 4px 2px 2px;
|
||||
padding: 6px;
|
||||
|
||||
a {
|
||||
color: var(--normal-color);
|
||||
text-decoration-thickness: 2px;
|
||||
|
||||
&:active, &:focus { color: var(--active-color); }
|
||||
&:visited { color: var(--active-border); }
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 2px solid var(--border-2);
|
||||
}
|
||||
}
|
||||
</style>
|
196
src/components/Updates.astro
Normal file
@ -0,0 +1,196 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import UpdateCard from "./UpdateCard.astro";
|
||||
|
||||
const updates = await getCollection("updates");
|
||||
---
|
||||
<section id="updates">
|
||||
<header>
|
||||
<h1>Updates</h1>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="inner">
|
||||
<h2>
|
||||
<div class="title">
|
||||
<span class="segment">Subject</span>
|
||||
<hr>
|
||||
<span class="segment">Date</span>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<UpdateCard title="redo gallery and guestbook" date="2024-05-10">
|
||||
{updates.length}
|
||||
<p>redid <a href="/gallery">gallery page</a> to make adding images easier and added alpinejs to <a href="/guestbook">guestbook</a> for fetching entries live</p>
|
||||
</UpdateCard>
|
||||
|
||||
<UpdateCard title="changed name" date="2024-05-09">
|
||||
<p>changed name from invicta to haetae since invicta doesn't fit anymore</p>
|
||||
</UpdateCard>
|
||||
|
||||
<footer id="updates-pagination">
|
||||
<a id="previous" aria-label="go to previous post" href="javascript:void(0)">
|
||||
<div class="arrow" />
|
||||
<div class="title">prev</div>
|
||||
</a>
|
||||
<a id="next" aria-label="go to next post" href="javascript:void(0)">
|
||||
<div class="title">next</div>
|
||||
<div class="arrow" />
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* UPDATES */
|
||||
#updates {
|
||||
& > header {
|
||||
background-color: var(--border-6);
|
||||
border: 2px solid var(--border-0);
|
||||
border-image: var(--headerBorder) 18 9 15 9 fill / 36px 18px 30px;
|
||||
padding: 36px 18px 30px;
|
||||
|
||||
h1 {
|
||||
font-family: var(--arial-font);
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
& > .content {
|
||||
background-color: var(--bg-0);
|
||||
border: 2px solid var(--border-0);
|
||||
border-image: var(--bottomBorder) 0 8 40 7 fill / 0px 16px 80px 14px;
|
||||
/* padding: 0px 8px 40px 7px; */
|
||||
padding: 0 10px 78px;
|
||||
|
||||
& > .inner {
|
||||
background-color: var(--bg-2);
|
||||
padding: 16px 6px;
|
||||
margin: 18px 20px;
|
||||
border: 2px solid var(--border-6);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: var(--arial-font);
|
||||
font-size: 1rem;
|
||||
padding: 6px 2px;
|
||||
border-block: 2px solid var(--border-3);
|
||||
background-color: var(--bg-0);
|
||||
|
||||
.title {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 2px 1fr;
|
||||
background-color: var(--bg-8);
|
||||
color: var(--bg-0);
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
padding: 5px 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: var(--border-7);
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* UPDATES PAGINATION */
|
||||
#updates-pagination {
|
||||
margin-top: 1em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
.arrow {
|
||||
width: 36px;
|
||||
height: 50px;
|
||||
background: var(--buttons) no-repeat top left;
|
||||
}
|
||||
|
||||
#previous {
|
||||
grid-column: 1 / 2;
|
||||
justify-self: left;
|
||||
|
||||
.arrow { background-position: 0 0; }
|
||||
&:hover .arrow { background-position: -36px 0; }
|
||||
&:active .arrow, &:focus .arrow { background-position: -72px 0; }
|
||||
&:disabled .arrow { background-position: -108px 0; }
|
||||
|
||||
.title {
|
||||
border-left: none;
|
||||
margin-left: -2px;
|
||||
padding-left: calc(0.5em + 2px);
|
||||
}
|
||||
}
|
||||
|
||||
#next {
|
||||
grid-column: 2 / -1;
|
||||
justify-self: right;
|
||||
|
||||
.arrow { background-position: -144px 0; }
|
||||
&:hover .arrow { background-position: -180px 0; }
|
||||
&:active .arrow, &:focus .arrow { background-position: -216px 0; }
|
||||
&:disabled .arrow { background-position: -252px 0; }
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-right: none;
|
||||
margin-right: -2px;
|
||||
padding-right: calc(0.5em + 2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* BASE STYLING FOR ARROW BUTTONS */
|
||||
a {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: stretch;
|
||||
color: var(--normal-color);
|
||||
text-decoration: none;
|
||||
|
||||
.title {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 0 0.5em;
|
||||
background-color: var(--bg-7);
|
||||
border: 2px solid var(--border-1);
|
||||
box-shadow:
|
||||
inset 2px 2px 0 0 var(--bg-0),
|
||||
inset 4px 4px 0 0 var(--bg-5),
|
||||
inset -2px -2px 0 0 var(--border-5),
|
||||
inset -4px -4px 0 0 var(--border-6);
|
||||
|
||||
@media screen and (468px > width) {
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(12ch + 1em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--active-color);
|
||||
.title { border-color: var(--active-border); }
|
||||
}
|
||||
|
||||
&:active, &:focus {
|
||||
color: var(--normal-color);
|
||||
.title {
|
||||
border-color: var(--border-1);
|
||||
box-shadow:
|
||||
inset -2px -2px 0 0 var(--bg-0),
|
||||
inset -4px -4px 0 0 var(--bg-5),
|
||||
inset 2px 2px 0 0 var(--border-5),
|
||||
inset 4px 4px 0 0 var(--border-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -23,7 +23,10 @@ function slugify(input: string) {
|
||||
const parser = marked.use({ gfm: true, breaks: true, });
|
||||
|
||||
const blog = defineCollection({
|
||||
loader: glob({ pattern: "*.{md,mdx}", base: "./src/content/blog" }),
|
||||
loader: glob({
|
||||
pattern: "*.{md,mdx}",
|
||||
base: "./src/content/blog"
|
||||
}),
|
||||
schema: rssSchema.extend({
|
||||
currently: z.object({
|
||||
mood: z.enum(moods).optional(),
|
||||
@ -98,4 +101,15 @@ const fics = defineCollection({
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog, fics, chapters };
|
||||
const updates = defineCollection({
|
||||
loader: glob({
|
||||
pattern: "*.{md,mdx}",
|
||||
base: "status",
|
||||
}),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
date: z.coerce.date(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog, fics, chapters, updates };
|
@ -2,7 +2,6 @@
|
||||
import type { MarkdownLayoutProps } from "astro";
|
||||
import { Font } from "astro:assets";
|
||||
import Layout from "./Layout.astro";
|
||||
import Navbar from "~/Navbar.astro";
|
||||
import Figure from "~/Figure.astro";
|
||||
|
||||
import border from "$/pmd-border.png";
|
||||
@ -20,8 +19,6 @@ const { frontmatter } = Astro.props;
|
||||
<Font cssVariable="--wondermail" preload />
|
||||
</Fragment>
|
||||
|
||||
<Navbar />
|
||||
|
||||
<main>
|
||||
{frontmatter.avatar && frontmatter.avatarText && (
|
||||
<Figure class="avatar" imagePath={frontmatter.avatar} alt={frontmatter.avatarText} />
|
||||
|
@ -2,7 +2,6 @@
|
||||
import { Font, Image } from "astro:assets";
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "./Layout.astro";
|
||||
import Navbar from "~/Navbar.astro";
|
||||
import formatDate from "@/utils/formatDate";
|
||||
import moods from "@/utils/moods";
|
||||
|
||||
@ -29,7 +28,6 @@ blog.sort((a, b) => b.data.pubDate!.valueOf() - a.data.pubDate!.valueOf());
|
||||
<Fragment slot="head">
|
||||
<slot name="head" />
|
||||
</Fragment>
|
||||
<Navbar />
|
||||
|
||||
<main>
|
||||
<article id={id}>
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { Font } from "astro:assets";
|
||||
import "$/styles/base.css";
|
||||
import "$/styles/themes.css";
|
||||
import Navbar from "~/Navbar.astro";
|
||||
|
||||
interface Props { title?: string; }
|
||||
|
||||
@ -25,6 +26,7 @@ const { title = "haetae" }: Props = Astro.props;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<Navbar />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
@ -24,6 +24,7 @@ const { Content, remarkPluginFrontmatter } = await render(chapter);
|
||||
|
||||
const chapters = await getCollection("chapters", ({ id }) => id.split("/")[0] === ficId);
|
||||
const fic = await getCollection("fics", ({ id }) => id === ficId);
|
||||
|
||||
chapters.sort((a, b) => a.data.sortOrder - b.data.sortOrder);
|
||||
const current = chapters.findIndex(chapter => chapter.id === `${ficId}/${chapterId}`);
|
||||
const next = current + 1 === chapters.length ? undefined : chapters[current + 1];
|
||||
|
@ -59,14 +59,14 @@ const notes = fic.rendered && await parser.parse((fic.rendered.metadata!.frontma
|
||||
</dl>
|
||||
|
||||
<div class="links">
|
||||
{chapters && <>
|
||||
{/* <a class="button" href={`/fics/${chapters[0].id}`}>start reading</a> */}
|
||||
{chapters.length > 0 && <>
|
||||
<a class="button" href={`/fics/${chapters[0].id}`}>start reading</a>
|
||||
<a class="button" href={`/fics/${Astro.params.ficId}/rss.xml`}>rss feed</a>
|
||||
</>}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{chapters && (
|
||||
{chapters.length > 0 && (
|
||||
<h2>chapters</h2>
|
||||
<ul>
|
||||
{chapters.map(chapter => (
|
||||
@ -75,6 +75,7 @@ const notes = fic.rendered && await parser.parse((fic.rendered.metadata!.frontma
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{fic.body && (
|
||||
<section id="oneshot">
|
||||
<ChapterContent lastModified={lastMod} notes={notes}>
|
||||
|
@ -14,21 +14,21 @@ chapters.sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueO
|
||||
|
||||
<h2>recent updates</h2>
|
||||
<ul>
|
||||
{chapters.map(post => (
|
||||
{chapters.map(chapter => (
|
||||
<li>
|
||||
<time datetime={formatDate(post.data.publishedAt, true)}>
|
||||
{formatDate(post.data.publishedAt, false, "MMMM DD, YYYY")}
|
||||
<time datetime={formatDate(chapter.data.publishedAt, true)}>
|
||||
{formatDate(chapter.data.publishedAt, false, "MMMM DD, YYYY")}
|
||||
</time>
|
||||
{fics.some(({ data }) => data.chapters?.some(({ id }) => id === post.id))
|
||||
{fics.some(({ data }) => data.chapters?.some(({ id }) => id === chapter.id))
|
||||
? <>
|
||||
<a href={`/fics/${post.id}`}>{post.data.title}</a>
|
||||
<a href={`/fics/${chapter.id}`}>{chapter.data.title}</a>
|
||||
in
|
||||
<a href={`/fics/${post.id.split("/")[0]}`}>
|
||||
{fics.filter(({ id }) => id === post.id.split("/")[0])[0].data.title}
|
||||
<a href={`/fics/${chapter.id.split("/")[0]}`}>
|
||||
{fics.find(({ id }) => id === chapter.id.split("/")[0])?.data.title}
|
||||
</a>
|
||||
</>
|
||||
: <a href={`/fics/${post.id.split("/")[0]}`}>
|
||||
{fics.filter(({ id }) => id === post.id.split("/")[0])[0].data.title}
|
||||
: <a href={`/fics/${chapter.id.split("/")[0]}`}>
|
||||
{fics.find(({ id }) => id === chapter.id.split("/")[0])?.data.title}
|
||||
</a>}
|
||||
</li>
|
||||
))}
|
||||
|
@ -30,8 +30,9 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {};
|
||||
<input type="url" id="website" name="website" aria-describedby="website-error" />
|
||||
{inputErrors.website && <p id="website-error" class="error">{inputErrors.website}</p>}
|
||||
|
||||
<!-- <label for="challengeQuestionAnswer">What's my name?</label>
|
||||
<input placeholder="read my about page!" type="text" id="challengeQuestionAnswer" name="challengeQuestionAnswer" required /> -->
|
||||
<label for="challenge">Challenge: What's my name?</label>
|
||||
<input placeholder="read my about page!" type="text" id="challenge" name="challenge" required aria-describedby="challenge-error" />
|
||||
{inputErrors.challenge && <p id="challenge-error" class="error">{inputErrors.challenge}</p>}
|
||||
|
||||
<label for="message">Message</label>
|
||||
<textarea placeholder="Message (plain text only)..." id="message" name="message" required aria-describedby="message-error"></textarea>
|
||||
|
@ -1,11 +1,13 @@
|
||||
---
|
||||
import { Font } from 'astro:assets';
|
||||
import Layout from '@/layouts/Layout.astro';
|
||||
import Navbar from '~/Navbar.astro';
|
||||
import Updates from '~/Updates.astro';
|
||||
|
||||
import outerBBS from "$/guild-bbs.png";
|
||||
import innerBBS from "$/guild-bbs-content.png";
|
||||
import sideBBS from "$/guild-bbs-list.png";
|
||||
import headerBBS from "$/guild-bbs-header.png";
|
||||
import bottomBBS from "$/guild-bbs-bottom.png";
|
||||
import buttons from "$/guild-bbs-buttons.png";
|
||||
---
|
||||
<Layout>
|
||||
@ -13,59 +15,59 @@ import buttons from "$/guild-bbs-buttons.png";
|
||||
<link rel="preload" href={outerBBS.src} as="image" />
|
||||
<link rel="preload" href={innerBBS.src} as="image" />
|
||||
<link rel="preload" href={sideBBS.src} as="image" />
|
||||
<link rel="preload" href={headerBBS.src} as="image" />
|
||||
<link rel="preload" href={bottomBBS.src} as="image" />
|
||||
<link rel="preload" href={buttons.src} as="image" />
|
||||
<Font cssVariable="--arial" preload />
|
||||
<Font cssVariable="--dotum-11" preload />
|
||||
<Font cssVariable="--dotumche-11" preload />
|
||||
</Fragment>
|
||||
|
||||
<Navbar />
|
||||
<main>
|
||||
<section id="updates">
|
||||
<article class="update card">
|
||||
<h1>update title</h1>
|
||||
<time datetime="">05/01/25</time>
|
||||
<div class="content">
|
||||
<p>some stuff happened</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<footer id="updates-pagination">
|
||||
<a id="previous" aria-label="go to previous post" href="javascript:void(0)">
|
||||
<div class="arrow" />
|
||||
<div class="title">prev</div>
|
||||
</a>
|
||||
<a id="next" aria-label="go to next post" href="javascript:void(0)">
|
||||
<div class="title">next</div>
|
||||
<div class="arrow" />
|
||||
</a>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<section id="welcome">
|
||||
<div class="card">
|
||||
<div class="inner">
|
||||
<h1>welcome!</h1>
|
||||
<article>
|
||||
this is
|
||||
<article class="card">
|
||||
<h2>waddup</h2>
|
||||
<div class="content">
|
||||
hi
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Updates />
|
||||
|
||||
<section id="wall">
|
||||
<div class="inner">
|
||||
<h1>stuff!</h1>
|
||||
<p>i keep a bunch of links here, including the webrings i've joined and some badges i like</p>
|
||||
<div class="web">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="scrobbler">
|
||||
|
||||
</section>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style define:vars={{ outerBorder: `url(${outerBBS.src})`, innerBorder: `url(${innerBBS.src})`, sideBorder: `url(${sideBBS.src})`, buttons: `url(${buttons.src})` }}>
|
||||
<style define:vars={{
|
||||
outerBorder: `url(${outerBBS.src})`,
|
||||
innerBorder: `url(${innerBBS.src})`,
|
||||
sideBorder: `url(${sideBBS.src})`,
|
||||
headerBorder: `url(${headerBBS.src})`,
|
||||
bottomBorder: `url(${bottomBBS.src})`,
|
||||
buttons: `url(${buttons.src})`
|
||||
}}>
|
||||
main {
|
||||
image-rendering: pixelated;
|
||||
--bg-0: #fff;
|
||||
--bg-1: #eeffff;
|
||||
--bg-2: #eeeeff;
|
||||
--bg-3: #eee;
|
||||
--bg-1: #f5fafc;
|
||||
--bg-2: #e8eef3;
|
||||
--bg-3: #e6e9eb;
|
||||
--bg-4: #ddddee;
|
||||
--bg-5: #ddeeee;
|
||||
--bg-6: #ccccdd;
|
||||
@ -78,6 +80,7 @@ import buttons from "$/guild-bbs-buttons.png";
|
||||
--border-4: #88aabb;
|
||||
--border-5: #99bbcc;
|
||||
--border-6: #bbccdd;
|
||||
--border-7: #aaaacc;
|
||||
--normal-color: #335577;
|
||||
--active-color: #aa0033;
|
||||
--active-border: #442266;
|
||||
@ -88,117 +91,69 @@ import buttons from "$/guild-bbs-buttons.png";
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin: 1em;
|
||||
|
||||
@media screen and (max-width: 1080px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
h1 { font: bold 1rem var(--arial-font); }
|
||||
|
||||
/* UPDATES */
|
||||
#updates {
|
||||
border-image: var(--sideBorder) 22 9 40 8 fill / 44px 18px 80px 16px;
|
||||
padding: 44px 18px 80px 16px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
#updates-pagination {
|
||||
margin: 1rem 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
.arrow {
|
||||
width: 36px;
|
||||
height: 50px;
|
||||
background: var(--buttons) no-repeat top left;
|
||||
}
|
||||
|
||||
#previous {
|
||||
grid-column: 1 / 2;
|
||||
justify-self: left;
|
||||
|
||||
.arrow { background-position: 0 0; }
|
||||
&:hover .arrow { background-position: -36px 0; }
|
||||
&:active .arrow, &:focus .arrow { background-position: -72px 0; }
|
||||
|
||||
.title {
|
||||
border-left: none;
|
||||
margin-left: -2px;
|
||||
padding-left: calc(0.5em + 2px);
|
||||
}
|
||||
}
|
||||
|
||||
#next {
|
||||
grid-column: 2 / -1;
|
||||
justify-self: right;
|
||||
|
||||
.arrow { background-position: -108px 0; }
|
||||
&:hover .arrow { background-position: -144px 0; }
|
||||
&:active .arrow, &:focus .arrow { background-position: -180px 0; }
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-right: none;
|
||||
margin-right: -2px;
|
||||
padding-right: calc(0.5em + 2px);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: stretch;
|
||||
color: var(--normal-color);
|
||||
text-decoration: none;
|
||||
|
||||
.title {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 0 0.5em;
|
||||
background-color: var(--bg-7);
|
||||
border: 2px solid var(--border-1);
|
||||
box-shadow:
|
||||
inset 2px 2px 0 0 var(--bg-0),
|
||||
inset 4px 4px 0 0 var(--bg-5),
|
||||
inset -2px -2px 0 0 var(--border-5),
|
||||
inset -4px -4px 0 0 var(--border-6);
|
||||
|
||||
@media screen and (468px > width) {
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(12ch + 1em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--active-color);
|
||||
.title { border-color: var(--active-border); }
|
||||
}
|
||||
text-decoration-thickness: 2px;
|
||||
|
||||
&:active, &:focus {
|
||||
color: var(--normal-color);
|
||||
.title {
|
||||
border-color: var(--border-1);
|
||||
box-shadow:
|
||||
inset -2px -2px 0 0 var(--bg-0),
|
||||
inset -4px -4px 0 0 var(--bg-5),
|
||||
inset 2px 2px 0 0 var(--border-5),
|
||||
inset 4px 4px 0 0 var(--border-6);
|
||||
}
|
||||
color: var(--active-color);
|
||||
text-decoration-color: var(--active-border);
|
||||
}
|
||||
}
|
||||
.inner {
|
||||
border: 2px solid var(--border-2);
|
||||
border-image: var(--innerBorder) 3 2 2 fill / 6px 4px 4px;
|
||||
padding: 4px;
|
||||
background-color: var(--bg-0);
|
||||
}
|
||||
|
||||
/* WELCOME */
|
||||
#welcome {
|
||||
border-image: var(--outerBorder) 20 9 39 11 fill / 40px 18px 78px 22px;
|
||||
padding: 40px 18px 78px 22px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border: 2px solid var(--border-0);
|
||||
border-image: var(--outerBorder) 19 9 38 11 fill / 38px 18px 76px 22px;
|
||||
background-color: var(--bg-2);
|
||||
padding: 36px 16px 74px 20px;
|
||||
margin-right: -2px;
|
||||
|
||||
.inner {
|
||||
flex: 1;
|
||||
margin: 14px 4px 8px;
|
||||
/* padding: 1rem; */
|
||||
}
|
||||
|
||||
/* INNER BORDERS */
|
||||
.inner {
|
||||
border-image: var(--innerBorder) 3 2 2 2 fill / 6px 4px 4px;
|
||||
padding: 6px 4px 4px;
|
||||
.card {
|
||||
h2 {
|
||||
font: normal 1.375rem var(--dotum-11-font);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1080px) {
|
||||
margin-right: 0;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
#wall {
|
||||
border-image: var(--sideBorder) 22 9 40 8 fill / 44px 18px 80px 16px;
|
||||
background-color: var(--bg-0);
|
||||
padding: 42px 10px 78px;
|
||||
margin-top: -2px;
|
||||
grid-column: 1 / -1;
|
||||
|
||||
& > .inner {
|
||||
background-color: var(--bg-2);
|
||||
padding: 16px 6px;
|
||||
margin: 18px 20px 16px;
|
||||
border: 2px solid var(--border-6);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -3,7 +3,7 @@ import utc from "dayjs/plugin/utc";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
export default function (date: Date | string, iso = false, format?: string) {
|
||||
export default function (date: string, iso = false, format?: string) {
|
||||
if (iso) {
|
||||
return dayjs(date).utc(true).toISOString();
|
||||
} else {
|
||||
|
5
status/changelog01.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: changed name
|
||||
date: 2024-05-09
|
||||
---
|
||||
changed name from invicta to haetae since invicta doesn't fit anymore
|
5
status/changelog02.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: redid gallery and guestbook
|
||||
date: 2024-05-10
|
||||
---
|
||||
redid the [gallery](/gallery) to make adding images easier and added alpinejs to [guestbook](/guestbook) for fetching entries live
|
5
status/changelog03.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: minfilia shrine
|
||||
date: 2024-05-13
|
||||
---
|
||||
make wip layout for minfilia shrine, added history and Thoughts(tm) about her
|
5
status/changelog04.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: redid homepage
|
||||
date: 2024-05-19
|
||||
---
|
||||
finally got around to making homepage pretty-ish
|
5
status/changelog05.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: add masaki and blog feed
|
||||
date: 2024-05-23
|
||||
---
|
||||
make a wip layout for masaki's page and added a feed for the blog
|
5
status/changelog06.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: add links and fix stuff
|
||||
date: 2024-05-24
|
||||
---
|
||||
added links in more and fixed some stuff in [guestbook](/guestbook) and [blog](/blog)
|