Compare commits

...

4 Commits

Author SHA1 Message Date
e384937596 remove astro:db and add drizzle 2025-09-22 16:35:22 -04:00
3c3d596c9c add updates and fix shippy 2025-09-22 16:34:59 -04:00
c8cce26569 get updates styled 2025-09-04 03:12:25 -04:00
f12810a424 add shippy and restructure index page 2025-09-03 03:24:55 -04:00
40 changed files with 5371 additions and 1237 deletions

View File

@ -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: [
{

1006
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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
View 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,
},
});

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

5
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,5 @@
onlyBuiltDependencies:
- esbuild
- sharp
nodeLinker: hoisted

10
shippy.config.ts Normal file
View 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"
}
};

View File

@ -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 });
}
},
}),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 B

After

Width:  |  Height:  |  Size: 715 B

View File

@ -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: "Authors Notes:";
font-weight: bold;
.author-note {
line-height: 1;
alt: "Author's Notes:";
font-weight: bold;
}
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
---
const api_key = "";
const username = "";
const url = "";
---
<span class="refresh" onclick="window.location.reload()">&#128472;</span>

View File

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

View 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>

View 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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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];

View File

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

View File

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

View File

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

View File

@ -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">
<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 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); }
a {
color: var(--normal-color);
text-decoration-thickness: 2px;
/* UPDATES */
#updates {
border-image: var(--sideBorder) 22 9 40 8 fill / 44px 18px 80px 16px;
padding: 44px 18px 80px 16px;
margin-right: -2px;
&:active, &:focus {
color: var(--active-color);
text-decoration-color: var(--active-border);
}
}
#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); }
}
&: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);
}
}
}
.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; */
}
.card {
h2 {
font: normal 1.375rem var(--dotum-11-font);
}
}
@media screen and (max-width: 1080px) {
margin-right: 0;
margin-bottom: -2px;
}
}
/* INNER BORDERS */
.inner {
border-image: var(--innerBorder) 3 2 2 2 fill / 6px 4px 4px;
padding: 6px 4px 4px;
#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>

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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)