move assets and implement meadow's guestbook

This commit is contained in:
haetae 2025-04-06 23:12:23 -04:00
parent 9e42d91a07
commit 6be223a2a8
29 changed files with 259 additions and 172 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/fonts/pixeab.woff2 Normal file

Binary file not shown.

BIN
public/fonts/pixearg.woff2 Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 411 B

View File

Before

Width:  |  Height:  |  Size: 689 B

After

Width:  |  Height:  |  Size: 689 B

View File

Before

Width:  |  Height:  |  Size: 169 B

After

Width:  |  Height:  |  Size: 169 B

View File

Before

Width:  |  Height:  |  Size: 731 B

After

Width:  |  Height:  |  Size: 731 B

View File

Before

Width:  |  Height:  |  Size: 699 B

After

Width:  |  Height:  |  Size: 699 B

View File

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 254 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View File

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View File

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 255 B

View File

Before

Width:  |  Height:  |  Size: 302 B

After

Width:  |  Height:  |  Size: 302 B

View File

@ -7,7 +7,7 @@
--title-font: "Kiwi Soda", Impact, Haettenschweiler, 'Arial Narrow Bold', sans-serif;
--mono-font: "Departure Mono", ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
--serif-font: "Redaction 35", 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
--sans-font: "MLSS", 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
--sans-font: "Super Star", 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
--arial-font: "Arial Pixel", Arial, Helvetica, sans-serif;
--pmd-font: "Wonder Mail", Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
--mplus-10-font: "PixelMPlus 10", "MS Gothic", system-ui-ja, system-ui-zh-cn, system-ui-zh-tw, system-ui-zh-hk, system-ui-ko, sans-serif;

View File

@ -1,12 +1,12 @@
@font-face {
font-family: "Arial Pixel";
src: url("/fonts/PIXEAR.woff2") format("woff2");
src: url("/fonts/pixearg.woff2") format("woff2");
font-weight: normal;
}
@font-face {
font-family: "Arial Pixel";
src: url("/fonts/PIXEAB.woff2") format("woff2");
src: url("/fonts/pixeab.woff2") format("woff2");
font-weight: bold;
}
@ -23,33 +23,21 @@
}
@font-face {
font-family: "MLSS";
font-family: "Super Star";
src: url("/fonts/mario-luigi-rpg-speech-text.woff2") format("woff2");
font-weight: normal;
}
@font-face {
font-family: "PixelMPlus 10";
src: url("/fonts/PixelMplus10-Regular.woff2") format("woff");
font-weight: normal;
}
@font-face {
font-family: "PixelMPlus 10";
src: url("/fonts/PixelMplus10-Bold.woff2") format("woff");
font-weight: bold;
}
@font-face {
font-family: "PixelMPlus 12";
src: url("/fonts/PixelMplus12-Regular.woff2") format("woff");
src: url("/fonts/PixelMplus10-Regular.woff2") format("woff2");
font-weight: normal;
}
@font-face {
font-family: "PixelMPlus 12";
src: url("/fonts/PixelMplus12-Bold.woff2") format("woff");
font-weight: bold;
src: url("/fonts/PixelMplus12-Regular.woff2") format("woff2");
font-weight: normal;
}
@font-face {

View File

@ -1,6 +1,9 @@
---
title: hmhmhm!
pubDate: 2024-02-04
currently:
mood: sad
reading: sad fanfic
---
hey there this is a test

View File

@ -1,11 +1,11 @@
---
import type { MarkdownLayoutProps } from "astro";
import Layout from "./Layout.astro";
import Navbar from "@/components/Navbar.astro";
import Figure from "@/components/Figure.astro";
import Navbar from "~/Navbar.astro";
import Figure from "~/Figure.astro";
import border from "@/assets/border.png";
import frame from "@/assets/frame.png";
import border from "$/images/border.png";
import frame from "$/images/frame.png";
type Props = MarkdownLayoutProps<{
avatar?: string;

View File

@ -4,12 +4,12 @@ import { getCollection } from "astro:content";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import Layout from "./Layout.astro";
import Navbar from "@/components/Navbar.astro";
import Navbar from "~/Navbar.astro";
import moods from "@/utils/moods";
import outerBBS from "@/assets/guild-bbs.png";
import innerBBS from "@/assets/guild-bbs-content.png";
import sideBBS from "@/assets/guild-bbs-list.png";
import outerBBS from "$/images/guild-bbs.png";
import innerBBS from "$/images/guild-bbs-content.png";
import sideBBS from "$/images/guild-bbs-list.png";
interface Props {
title: string;
@ -36,7 +36,7 @@ dayjs.extend(utc);
<main>
<nav id="blog-links">
<div class="inner">
<hgroup>
<div class="nav-section">
<h1><span class="title">recent posts</span></h1>
<ul>
{blog.map(entry => (
@ -50,76 +50,74 @@ dayjs.extend(utc);
</li>
))}
</ul>
</hgroup>
<hgroup>
</div>
<div class="nav-section">
<h1><span class="title">other links</span></h1>
<ul>
<li><span class="item"><a href="/blog">archive</a></span></li>
<li><span class="item"><a href="/blog/rss.xml">rss feed</a></span></li>
</ul>
</hgroup>
</div>
</div>
</nav>
<section>
<article>
<div class="inner">
<header>
<h1>{title}</h1>
<hr />
<div class="info">
<time datetime={dayjs(date).utc(true).toISOString()}>
<span class="title">Date</span>
<span class="desc">{dayjs(date).utc(true).format("MMMM DD, YYYY")}</span>
</time>
</div>
</header>
<div class="content" data-simplebar>
<slot />
<article>
<div class="inner">
<header>
<h1>{title}</h1>
<hr />
<div class="info">
<time datetime={dayjs(date).utc(true).toISOString()}>
<span class="title">Date</span>
<span class="desc">{dayjs(date).utc(true).format("MMMM DD, YYYY")}</span>
</time>
</div>
</header>
{currently && (
<aside>
<h2>Current</h2>
{currently?.mood && (
<dl>
<dt class="title">Mood</dt>
<dd class="desc">
<Image src={`/src/assets/moods/${moods.find(mood => mood === currently?.mood)}.png`} width={36} height={36} alt="" />
{currently.mood}
</dd>
</dl>
)}
{currently.playing && (
<dl>
<dt class="title">Game</dt>
<dd class="desc">{currently.playing}</dd>
</dl>
)}
{currently.watching && (
<dl>
<dt class="title">Show</dt>
<dd class="desc">{currently.watching}</dd>
</dl>
)}
{currently.reading && (
<dl>
<dt class="title">Book</dt>
<dd class="desc">{currently.reading}</dd>
</dl>
)}
{currently.listening && (
<dl>
<dt>Currently listening</dt>
<dd>{currently.listening}</dd>
</dl>
)}
</aside>
)}
<div class="content">
<slot />
</div>
</article>
</section>
{currently && (
<aside>
<h2>Current</h2>
{currently?.mood && (
<dl>
<dt class="title">Mood</dt>
<dd class="desc">
<Image src={`/src/assets/moods/${moods.find(mood => mood === currently?.mood)}.png`} width={36} height={36} alt="" />
{currently.mood}
</dd>
</dl>
)}
{currently.playing && (
<dl>
<dt class="title">Game</dt>
<dd class="desc">{currently.playing}</dd>
</dl>
)}
{currently.watching && (
<dl>
<dt class="title">Show</dt>
<dd class="desc">{currently.watching}</dd>
</dl>
)}
{currently.reading && (
<dl>
<dt class="title">Book</dt>
<dd class="desc">{currently.reading}</dd>
</dl>
)}
{currently.listening && (
<dl>
<dt>Currently listening</dt>
<dd>{currently.listening}</dd>
</dl>
)}
</aside>
)}
</div>
</article>
<slot name="pagination" />
</main>
</Layout>
@ -238,7 +236,7 @@ dayjs.extend(utc);
}
}
hgroup {
.nav-section {
margin: 0 4px;
background-color: var(--bg-0);
@ -317,7 +315,7 @@ dayjs.extend(utc);
.content {
position: relative;
background-attachment: local;
background-image: linear-gradient(to right, var(--bg-0) 5px, transparent 2px), linear-gradient(var(--bg-4) 2px, transparent 2px);
background-image: linear-gradient(to right, var(--bg-0) 5px, transparent 2px), linear-gradient(var(--bg-4) 2px, transparent 2px);
background-size: 10px 1lh;
background-position-y: calc(2lh - 2px);
margin: 2rem 1rem;

View File

@ -1,5 +1,5 @@
---
import "@/assets/styles/base.css";
import "$/styles/base.css";
interface Props { title?: string; }
@ -16,9 +16,9 @@ const { title = "haetae" }: Props = Astro.props;
<slot name="header" />
<title>{title}</title>
<script is:inline>
localStorage.getItem("theme") ?
document.documentElement.className = localStorage.getItem("theme") :
document.documentElement.removeAttribute("class");
localStorage.getItem("theme")
? document.documentElement.className = localStorage.getItem("theme")
: document.documentElement.removeAttribute("class");
</script>
</head>
<body>

View File

@ -1,12 +1,28 @@
---
title: about me
layout: ../layouts/About.astro
avatar: /src/assets/portrait-0025.png
avatar: /src/assets/images/portrait-0025.png
avatarText: pikachu from pokemon mystery dungeon
---
# hello!
it's me, the weirdo. here's a bunch of my images:
hey there, i go by haetae (or tae, for short) and i use he/him pronouns.
## badges...
# credits
i used a bunch of assets that require attribution but i also wanted to link people to the authors because their works are great!
## images
- [Pikachu portrait](https://sprites.pmdcollab.org/#/0025?form=0) by SPIKE CHUNSOFT, curated by the PMD Collab project
- [This about page's frame images](https://www.spriters-resource.com/ds_dsi/pokemonmysterydungeonexplorersoftimedarkness/sheet/5986/) by redblueyellow
- The blog's frame images and additional assets (such as the mood emoticons) were graciously provided by AymShade on the MapleLegends forums! I can't directly link because I'm scared of a Certain Company™ being evil
- [The guestbook's speech bubble images](https://www.spriters-resource.com/game_boy_advance/mlss/sheet/7573/) by MajinPiccolo
## fonts
- [Departure Mono](https://departuremono.com/) by [Helena Zhang](https://www.helenazhang.com/) is licensed under [OFL 1.1](https://www.tldrlegal.com/license/open-font-license-ofl-explained)
- [Kiwi Soda](https://fontenddev.com/fonts/kiwi-soda/) from fontenddev.com is licensed under [CC by 4.0](https://creativecommons.org/licenses/by/4.0/)
- [Mario & Luigi RPG Speech Text](https://fontstruct.com/fontstructions/show/1102228) by DarkMaxX is licensed under [CC by Share Alike 3.0](http://creativecommons.org/licenses/by-sa/3.0/)
- [PixelMplus](https://itouhiro.hatenablog.com/entry/20130602/font) by itouhiro is licensed under [M+ font license](https://web.archive.org/web/20221024231351/http://mplus-fonts.osdn.jp/mplus-bitmap-fonts/#license)
- [Redaction](https://www.redaction.us/) by [Forest Young](https://www.moma.org/interactives/exhibitions/2011/talktome/objects/140027/) and [Jeremy Mickel](https://mckltype.com/) is dual-licensed under [LGPL 2.1](https://www.tldrlegal.com/license/gnu-lesser-general-public-license-v2-1-lgpl-2-1) and [OFL 1.1](https://www.tldrlegal.com/license/open-font-license-ofl-explained)
- [sq](https://github.com/leahneukirchen/sq) by [Leah Neukirchen](https://leahneukirchen.org/) is in public domain
- [Wonder Mail](https://www.dafont.com/wonder-mail.font) by ShinxHijinx is free for personal use

View File

@ -2,12 +2,12 @@
import Blog from '@/layouts/Blog.astro';
import type { GetStaticPaths } from 'astro';
import { getCollection, render } from 'astro:content';
import leftNormal from "@/assets/left-normal.png";
import leftHover from "@/assets/left-hover.png";
import leftActive from "@/assets/left-active.png";
import rightNormal from "@/assets/right-normal.png";
import rightHover from "@/assets/right-hover.png";
import rightActive from "@/assets/right-active.png";
import leftNormal from "$/images/left-normal.png";
import leftHover from "$/images/left-hover.png";
import leftActive from "$/images/left-active.png";
import rightNormal from "$/images/right-normal.png";
import rightHover from "$/images/right-hover.png";
import rightActive from "$/images/right-active.png";
export const getStaticPaths = (async () => {
const blog = await getCollection("blog");

View File

@ -1,65 +1,98 @@
---
import { actions, isInputError } from "astro:actions";
import { db, Guestbook as table } from "astro:db";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import Layout from "@/layouts/Layout.astro";
import ThemeSwitch from "@/components/ThemeSwitch.astro";
import ThemeSwitch from "~/ThemeSwitch.astro";
import speech from "$/images/speech.png";
export const prerender = false;
type GuestBookEntry = {
ID: number;
CreatedAt: string;
UpdatedAt: string;
DeletedAt: null | string;
Name: string;
Text: string;
Website: null | string;
Approved: true;
GuestBookID: number;
Guestbook: object;
}
dayjs.extend(utc);
const result = Astro.getActionResult(actions.guestbook);
const errors = isInputError(result?.error) ? result.error.fields : {};
const entries = await db.select().from(table);
entries.sort((a, b) => b.date.valueOf() - a.date.valueOf());
const response = await fetch("https://guestbooks.meadow.cafe/api/v1/get-guestbook-messages/500");
const entries: Array<GuestBookEntry> = await response.json();
entries.sort((a, b) => new Date(b.CreatedAt).valueOf() - new Date(a.CreatedAt).valueOf());
---
<Layout title="haetae, guestbook">
<ThemeSwitch />
<main>
<h1>Guestbook</h1>
<details>
<summary>click me for a secret!</summary>
<noscript>you should put ILIAD, but in all lowercase, in the password field!</noscript>
<p id="cool-surprise"></p>
</details>
<form action={actions.guestbook} method="post">
<label for="username">Username</label>
<input type="text" name="username" id="username" aria-describedby="username-error" required />
{errors.username && <p id="username-error">{errors.username.join(",")}</p>}
<section id="form">
<form action="https://guestbooks.meadow.cafe/guestbook/500/submit" method="post" aria-describedby="errors">
<label for="name">Nickname</label>
<input type="text" id="name" name="name" required />
<label for="website">Website (optional)</label>
<input type="url" name="website" id="website" aria-describedby="website-error" />
{errors.website && <p id="website-error">{errors.website.join(",")}</p>}
<label for="website">Website (optional)</label>
<input type="url" id="website" name="website" />
<label for="body">Message</label>
<textarea name="body" id="body" rows="5" aria-describedby="body-error" required></textarea>
{errors.body && <p id="body-error">{errors.body.join(",")}</p>}
<label for="challengeQuestionAnswer">What's my name?</label>
<input placeholder="read my about page!" type="text" id="challengeQuestionAnswer" name="challengeQuestionAnswer" required />
<label for="password">Do you know the password?</label>
<input type="text" name="password" id="password" value="yes!" aria-describedby="password-error" required />
{errors.password && <p id="password-error">{errors.password.join(",")}</p>}
<label for="text">Message</label>
<textarea placeholder="Message (plain text only)..." id="text" name="text" required></textarea>
<button type="submit">Post</button>
</form>
<button type="submit">Submit</button>
</form>
<div id="errors" role="alert"></div>
{entries.map(({ username, website, body, date }) => (
<article class="entry">
<header>
<h1>{username}</h1>
<time datetime={dayjs(date).utc(true).toISOString()}>
Posted on {dayjs(date).utc(true).format("MMMM DD, YYYY")}
</time>
{website && <a href={website} target="_blank" referrerpolicy="no-referrer">website</a>}
</header>
{body}
</article>
))}
<aside>
Lovingly made with <a href="https://guestbooks.meadow.cafe" target="_blank" referrerpolicy="no-referrer">Guestbooks</a>
</aside>
</section>
<hr />
<section id="entries">
<h2>Messages</h2>
{entries.map(({ Name, Website, Text, CreatedAt}) => (
<article class="entry">
<header>
<h1>
{Website
? <a href={Website} target="_blank" referrerpolicy="no-referrer">{Name}</a>
: <span>{Name}</span>
}
</h1>
<time datetime={dayjs(CreatedAt).utc(true).toISOString()}>
Posted on {dayjs(CreatedAt).utc(true).format("MMMM DD, YYYY")}
</time>
</header>
{Text}
</article>
))}
</section>
<button onclick="document.getElementById('notification').showModal()">open popup</button>
<dialog id="notification">
<form method="dialog">
Successfully posted! Messages are manually approved. New entries should show up in a day or two! :)
<menu>
<button type="submit">Close</button>
</menu>
</form>
</dialog>
</main>
</Layout>
<style>
<style define:vars={{ border: `url(${speech.src})` }}>
:root {
--speech-bg-color: #f8f8f8;
--speech-fg-color: #404040;
}
main {
max-width: clamp(75ch, 80ch, 100%);
margin: 1rem auto;
@ -80,43 +113,91 @@ entries.sort((a, b) => b.date.valueOf() - a.date.valueOf());
}
}
form {
display: flex;
flex-flow: column wrap;
#form {
form {
display: flex;
flex-flow: column wrap;
input, textarea {
margin-bottom: 1rem;
padding: 2px 6px;
input, textarea {
margin-bottom: 1rem;
padding: 2px 6px;
}
}
aside {
color: color-mix(in oklab, var(--fg-color) 80%, var(--bg-color));
}
}
details {
& > summary {
list-style: none;
cursor: pointer;
font-weight: bold;
.entry {
image-rendering: pixelated;
font-family: var(--sans-font);
font-size: calc(1rem * 2);
letter-spacing: 1px;
color: var(--speech-fg-color);
background-color: var(--speech-bg-color);
border-image: var(--border) 7 / 7px / 7px repeat;
margin: 2rem 0;
}
&::after {
content: "[+]";
}
#notification {
margin: auto;
max-width: 35ch;
color: var(--fg-color);
background: var(--bg-color);
transition:
display 1s allow-discrete,
overlay 1s allow-discrete;
animation: fadeOut 1s forwards;
[open] &::after {
content: "[_]";
}
menu {
margin: 1em 0 0;
padding: 0;
display: flex;
justify-content: center;
}
&[open] {
animation: fadeIn 1.0s forwards;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
<script>
const details = document.getElementsByTagName("details").item(0);
const pw = document.getElementById("cool-surprise");
let initial = "vyvnq";
details?.addEventListener("toggle", e => {
if ((e.target as HTMLDetailsElement).open) {
pw!.innerHTML = `if you read this, put <strong>${initial.replace(/[a-zA-Z]/g, c => {
// @ts-expect-error the below code works
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
})}</strong> for the password field!`
const form = document.forms[0];
const challenge = form.elements["challengeQuestionAnswer" as any];
const errors = document.getElementById("errors")!;
const notification = document.getElementById("notification")! as HTMLDialogElement;
form.addEventListener("submit", async e => {
e.preventDefault();
let data = new FormData(form);
const response = await fetch(form.action, {
method: "POST",
body: data,
});
if (response.ok) {
form.reset();
notification.showModal();
errors.innerHTML = "";
} else {
const err = await response.text();
if (response.status === 401) {
challenge.ariaInvalid = "true";
errors.innerHTML = "The provided answer to the challenge question was invalid!";
} else {
errors.innerHTML = err;
}
}
});
</script>

View File

@ -5,8 +5,9 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["src/components/*"],
"$/*": ["src/assets/*"],
"@/*": ["src/*"],
}
}
}