This commit is contained in:
haetae 2025-01-19 23:21:07 -05:00
commit cfb3379ef0
52 changed files with 1310 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

48
README.md Normal file
View File

@ -0,0 +1,48 @@
# Astro Starter Kit: Basics
```sh
npm create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

13
astro.config.mjs Normal file
View File

@ -0,0 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import db from '@astrojs/db';
import node from '@astrojs/node';
// https://astro.build/config
export default defineConfig({
site: "https://haetae.gay",
integrations: [db()],
adapter: node({
mode: 'standalone'
})
});

BIN
bun.lockb Executable file

Binary file not shown.

14
db/config.ts Normal file
View File

@ -0,0 +1,14 @@
import { column, defineDb, defineTable, NOW } from 'astro:db';
const Guestbook = defineTable({
columns: {
username: column.text(),
website: column.text({ optional: true }),
body: column.text(),
date: column.date({ default: NOW }),
},
});
export default defineDb({
tables: { Guestbook },
});

9
db/seed.ts Normal file
View File

@ -0,0 +1,9 @@
import { db, Guestbook } from 'astro:db';
// https://astro.build/db/seed
export default async function seed() {
await db.insert(Guestbook).values([
{ username: "tester", website: "", body: "hey there!" },
{ username: "ayo", website: "https://google.com", body: "this is googlebot" },
]);
}

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "astro",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/db": "^0.14.5",
"@astrojs/node": "^9.0.1",
"@astrojs/rss": "^4.0.11",
"astro": "^5.1.7",
"astro-breadcrumbs": "^3.3.1",
"markdown-it": "^14.1.0",
"sanitize-html": "^2.14.0"
},
"devDependencies": {
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.10.7",
"@types/sanitize-html": "^2.13.0"
}
}

9
public/favicon.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

BIN
public/fonts/KiwiSoda.woff2 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

82
public/pretty-feed-v3.xsl Normal file

File diff suppressed because one or more lines are too long

52
src/actions/index.ts Normal file
View File

@ -0,0 +1,52 @@
import { defineAction, ActionError } from "astro:actions";
import { db, Guestbook } from "astro:db";
import { z } from "astro:schema";
import { checkProfanity } from "./utils";
export const server = {
guestbook: defineAction({
accept: "form",
input: z.object({
username: z.string(),
website: z.string().url().optional(),
body: z.string(),
honeypot: z.string().max(0).nullish(),
}),
handler: async (input) => {
if (input.username === "") {
throw new ActionError({
code: "BAD_REQUEST",
message: "You should put in a name for yourself!"
});
}
if (input.body === "") {
throw new ActionError({
code: "BAD_REQUEST",
message: "There should be a message here."
});
}
if (input.honeypot !== undefined) {
throw new ActionError({
code: "UNPROCESSABLE_CONTENT",
message: "Oh dear, something went wrong!",
});
}
const filter = await checkProfanity(input.body);
if (filter) {
return await db.insert(Guestbook).values({
username: input.username,
website: input.website,
body: input.body,
}).returning();
} else {
throw new ActionError({
code: "BAD_REQUEST",
message: "You can't curse!",
});
}
},
}),
}

29
src/actions/utils.ts Normal file
View File

@ -0,0 +1,29 @@
export async function checkProfanity(message: string) {
try {
const response = await fetch("https://vector.profanity.dev", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
if (!response.ok) {
throw new Error(`There was an error checking your message: ${response.status}`);
}
const json = await response.json();
if (json.isProfanity) {
throw new Error("Please don't cuss!");
}
return true;
} catch (e) {
console.error(e);
return false;
}
}
async function checkSpam(form: FormData) {
const token = form.append("procaptcha-response", import.meta.env.SPAM_SITE_KEY);
}

1
src/assets/astro.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/frame.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

View File

@ -0,0 +1,40 @@
@import url("./reset.css");
@import url("./fonts.css");
:root {
--body-font: "Wonder Mail", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--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;
}
body {
font-family: var(--body-font);
font-size: 2rem;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--title-font);
font-weight: normal;
line-height: 1;
}
h1 { font-size: 4rem; }
h2 { font-size: 3rem; }
h3 { font-size: 2rem; }
h4, h5, h6 { font-size: 1rem; }
@media screen and (max-width: 48em) {
h1, h2, h3 {
font-size: 3rem;
}
}
input, button, textarea, select {
font-family: var(--mono-font);
font-size: 1.375rem;
/* @media screen and (max-width: 48em) {
font-size: 11px;
} */
}

View File

@ -0,0 +1,38 @@
@font-face {
font-family: "Departure Mono";
src: url("/fonts/DepartureMono-Regular.woff2") format("woff2");
font-weight: normal;
}
@font-face {
font-family: "Kiwi Soda";
src: url("/fonts/KiwiSoda.woff2") format("woff2");
font-weight: normal;
}
@font-face {
font-family: "Redaction 35";
src: url("/fonts/Redaction_35-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Redaction 35";
src: url("/fonts/Redaction_35-Italic.woff2") format("woff2");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "Redaction 35";
src: url("/fonts/Redaction_35-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "Wonder Mail";
src: url("/fonts/wondermail.woff2") format("woff2");
font-weight: normal;
}

View File

@ -0,0 +1,37 @@
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100vh;
width: 100vw;
}
body {
line-height: calc(1em + 0.5rem);
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
}
h1, h2, h3, h4, h5, h6 {
text-wrap: balance;
}

View File

@ -0,0 +1,17 @@
---
import { Image } from 'astro:assets';
interface Props {
imagePath: string;
alt: string;
caption?: string;
}
const { imagePath, alt, caption }: Props = Astro.props;
const images = import.meta.glob<{ default: ImageMetadata }>("/src/assets/**/*.{jpeg,jpg,png,webp,gif}");
if (!images[imagePath]) throw new Error(`"${imagePath}" does not exist in glob: "src/assets/**/*.{jpeg,jpg,png,webp,gif}"`);
---
<figure>
<Image src={images[imagePath]()} {alt} />
<figcaption>{caption ?? alt}</figcaption>
</figure>

View File

@ -0,0 +1,32 @@
---
const links = [
"about",
"blog",
"fics",
"gallery",
"guestbook",
];
---
<nav>
<ul>
<li><a href="/">index</a></li>
{links.map(link => (
<li><a href={`/${link}`}>{link}</a></li>
))}
</ul>
</nav>
<style>
nav {
font-family: var(--title-font);
font-size: 2rem;
ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 1rem;
}
}
</style>

12
src/components/Top.astro Normal file
View File

@ -0,0 +1,12 @@
<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,14 @@
---
title: hey girl hey
pubDate: 2024-02-03
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin pretium augue elit, eget interdum massa lobortis ut. Praesent facilisis ornare aliquam. Donec sit amet volutpat ipsum, id ultricies urna. Donec vestibulum sagittis felis, tempor fermentum urna posuere at. Vestibulum cursus mauris eget bibendum blandit. Aenean at augue porttitor, bibendum massa ut, laoreet lacus. Phasellus fermentum tincidunt lectus vel volutpat. Proin sagittis vel sem sit amet consequat. Vestibulum ac laoreet quam. Mauris eu purus sit amet odio maximus dictum. Mauris quam tellus, tempus eu faucibus in, mollis quis velit. Phasellus nisl mauris, congue vel magna a, rutrum aliquet ante. Proin in ante pharetra, vestibulum nisi vel, fringilla tortor. Etiam mattis, mauris et mattis sagittis, orci eros ornare risus, vitae consequat ligula nulla eget quam.
Fusce malesuada sed risus eget elementum. Quisque porttitor finibus libero, et semper magna pretium ac. Donec gravida erat iaculis ante cursus, in laoreet ligula dapibus. Vestibulum gravida lorem eleifend mollis hendrerit. Nulla et velit dapibus, aliquet lectus at, mattis nulla. Pellentesque lacus eros, sagittis quis dignissim a, malesuada ut eros. Aliquam fringilla a leo sed commodo. Sed eu ligula malesuada, ultrices orci eget, suscipit ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras interdum libero sed leo eleifend tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
Vivamus scelerisque ac quam sed interdum. Mauris pulvinar aliquet est in luctus. Donec convallis dui aliquam urna pellentesque, eget maximus tellus scelerisque. Integer posuere commodo justo in finibus. Vivamus at molestie nisl, sit amet ultrices ex. Ut elementum dignissim dui a lacinia. Etiam nec purus ac felis congue tristique ut eget leo. Aenean id augue molestie, auctor quam ac, aliquam magna. Maecenas condimentum sem mauris, in sodales nunc suscipit non. Sed iaculis ut magna vel sollicitudin. Etiam commodo lacinia lorem, quis tincidunt sem laoreet et. Sed interdum elit ac erat blandit cursus. Pellentesque imperdiet placerat lacus id sagittis. In vitae efficitur ante, sit amet feugiat nunc. Praesent erat mi, hendrerit molestie maximus sed, tempus eu massa.
Fusce convallis ultricies orci, vulputate laoreet magna. Proin in aliquet diam. Etiam placerat ante eget lacus ultrices fermentum. Phasellus interdum facilisis ex mattis blandit. Quisque vitae convallis velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sodales tincidunt lorem. Ut bibendum posuere elementum. Phasellus arcu lacus, porta sit amet convallis a, finibus et nisi. Sed id justo dapibus, faucibus elit eu, tincidunt ante.
Donec vehicula ultrices egestas. Maecenas non magna tortor. Curabitur metus sapien, ultricies porta urna vitae, pulvinar blandit tellus. In aliquet risus sed libero vulputate, in volutpat ligula malesuada. Phasellus viverra pretium turpis, quis congue dolor fringilla vitae. Morbi orci lacus, mollis non dolor nec, euismod aliquet risus. Ut massa nibh, maximus sit amet turpis et, dapibus ullamcorper turpis. Aenean ut tellus ac nisi mollis tempus. Duis at libero quis felis ornare viverra ac sit amet justo. Sed gravida magna ut nibh tincidunt, consectetur lobortis est consequat. Phasellus ex velit, tincidunt sit amet consectetur id, vulputate eget tellus. Donec ligula elit, vestibulum non tincidunt eu, ullamcorper eget massa. Nulla venenatis maximus metus, sed pulvinar dolor ullamcorper at. Vestibulum volutpat tortor ante, a ornare justo porttitor tempus. Aliquam sit amet sem porta, hendrerit dolor sed, tristique neque.

View File

@ -0,0 +1,6 @@
---
title: hmhmhm!
pubDate: 2024-02-04
---
hey there this is a test

39
src/content/config.ts Normal file
View File

@ -0,0 +1,39 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
import { rssSchema } from "@astrojs/rss";
const blog = defineCollection({
loader: glob({ pattern: "*.md", base: "./src/content/blog" }),
schema: rssSchema,
});
function generateFicSlug({ entry, data }: { entry: string, data: any }): string {
if (data.slug) {
return data.slug as string;
}
return entry.split("/")[0];
}
const source = "./src/content/fics";
const chapters = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx,mdoc}", base: source }),
schema: z.object({
title: z.string(),
publishedAt: z.coerce.date(),
notes: z.ostring(),
lastModified: z.coerce.date().optional(),
sortOrder: z.number(),
}),
});
const fics = defineCollection({
loader: glob({ pattern: "**/*.{yml,yaml}", base: source, generateId: generateFicSlug }),
schema: z.object({
title: z.string(),
series: z.array(z.string()),
publishedAt: z.coerce.date(),
summary: z.string(),
}),
});
export const collections = { blog, fics, chapters };

View File

@ -0,0 +1,9 @@
title: hello world
series:
- soccer
- some what
publishedAt: 2024-01-20
summary:
sometime after the war, x finds love in the middle of a soccer field...
what wil lyou do now

View File

@ -0,0 +1,12 @@
---
title: not a sin
publishedAt: 2024-02-01
sortOrder: 1
notes:
hello is this is a <em>test</em>, this is going to be a really long text! this is where all the author's notes will go.
fhi
---
Lorem ipsum dolor sit amet *consectetur* adipisicing elit. Reiciendis reprehenderit provident ullam sint explicabo quas esse velit, voluptatum eveniet, tempora illum expedita, eum voluptate! Odio excepturi similique ex quos **tenetur**.
"here lies by her's"

View File

@ -0,0 +1,13 @@
---
title: this IS a sin
publishedAt: 2024-03-02
sortOrder: 2
notes:
<p>hey there this is just a test </p>
<p>him!!! <em>lol!!!</em></p>
---
Lorem ipsum dolor sit amet consectetur adipisicing elit. Reiciendis **reprehenderit** provident ullam sint *explicabo* quas esse velit, voluptatum eveniet, tempora illum expedita, eum voluptate! Odio excepturi similique ex quos tenetur.
"here lies by her's"
hmmm there it is!! "hiii!!!"

View File

@ -0,0 +1,6 @@
title: testing
series:
- another fandom
publishedAt: 2024-01-20
summary:
in another planet, we all face the end of a world

View File

@ -0,0 +1,7 @@
---
title: chapter 1
publishedAt: 2024-02-01
sortOrder: 1
---
# this is another test!!

11
src/layouts/About.astro Normal file
View File

@ -0,0 +1,11 @@
---
import Navbar from "@/components/Navbar.astro";
import Layout from "./Layout.astro";
---
<Layout>
<Navbar />
<main>
<slot />
</main>
</Layout>

153
src/layouts/Blog.astro Normal file
View File

@ -0,0 +1,153 @@
---
import { getCollection } from "astro:content";
import Layout from "./Layout.astro";
import Navbar from "@/components/Navbar.astro";
interface Props {
title: string;
date: Date;
}
const blog = await getCollection("blog");
blog.length = Math.min(blog.length, 5);
blog.sort((a, b) => a.data.pubDate!.valueOf() - b.data.pubDate!.valueOf());
const { title, date } = Astro.props;
---
<Layout>
<Navbar />
<section>
<nav id="blog-links">
<h1>recent posts</h1>
<ul>
{blog.map(entry => (
<li>
<time datetime={entry.data.pubDate!.toISOString()}>
{entry.data.pubDate?.toLocaleDateString(undefined, { dateStyle: "short" })}
</time>
<a href={`/blog/${entry.id}`}>{entry.data.title}</a>
</li>
))}
</ul>
<h1>other links</h1>
<ul>
<li><a href="/blog">archive</a></li>
<li><a href="/blog/rss.xml">rss feed</a></li>
</ul>
</nav>
<main>
<article>
<header>
<h1>{title}</h1>
<time datetime={date.toISOString()}>
Posted on
{date.toLocaleDateString(undefined, { dateStyle: "long" })}
</time>
</header>
<div class="content">
<slot />
</div>
</article>
<slot name="pagination" />
</main>
</section>
</Layout>
<style>
section {
display: grid;
grid-template-columns: repeat(2, auto);
justify-content: center;
gap: 1rem;
height: calc(100% - 2.5rem);
}
#blog-links {
min-width: 28ch;
border: 2px solid black;
border-bottom: none;
margin-top: 2rem;
padding: 0.5rem;
h1 {
font-size: 2rem;
margin: 1rem 1rem 0.25rem;
}
ul {
display: flex;
flex-flow: column wrap;
gap: 0.5rem;
list-style: none;
padding: 0;
li {
display: flex;
padding: 0 0.5rem;
justify-content: space-between;
align-items: center;
}
}
@media screen and (max-width: 1251px) {
border-bottom: 2px solid black;
}
}
main {
width: 75ch;
@media screen and (max-width: 950px) {
width: 100%;
margin: 0 1rem;
}
}
article {
header {
padding: 1rem;
padding-left: 2rem;
time { padding-left: 3rem; }
@media screen and (max-width: 950px) {
padding: 0.25rem;
padding-left: 2rem;
}
}
.content {
position: relative;
background-image: linear-gradient(#d1d5db 2px, transparent 0px);
background-size: 100% 1em;
background-position-y: 1.75rem;
padding: 2rem;
font-size: 2rem;
line-height: 1em;
border: 2px solid black;
p { margin-block-end: 1em; }
&::before, &::after {
position: absolute;
bottom: 100%;
left: 30px;
content: "";
border: 20px solid transparent;
}
&::before { border-bottom: 20px solid black; }
&::after {
border-bottom: 20px solid white;
margin-bottom: -3px;
}
@media screen and (max-width: 950px) {
background-position-y: 0.8rem;
padding: 1rem;
}
}
}
</style>

67
src/layouts/Chapter.astro Normal file
View File

@ -0,0 +1,67 @@
---
import Layout from "./Layout.astro";
interface Props {
title: string;
date: Date;
notes?: string;
lastModified?: string;
}
const { title, date, notes, lastModified }: Props = Astro.props;
---
<Layout>
<slot name="breadcrumbs" />
<main>
<header>
<h1>{title}</h1>
<time datetime={date.toISOString()}>
{date.toLocaleDateString(undefined, {
weekday: "long", year: "numeric", month: "long", day: "numeric",
})}
</time>
{lastModified && (
<time datetime={lastModified}>{lastModified}</time>
)}
{notes && (
<blockquote><Fragment set:html={notes.split("\n").join("<br />")} /></blockquote>
)}
</header>
<article>
<slot />
</article>
<slot name="pagination" />
</main>
</Layout>
<style>
main {
max-width: 80ch;
margin: 1rem auto 2rem;
}
article, blockquote { font-family: var(--serif-font); }
blockquote {
margin: 1rem;
font-size: 1.125rem;
&::before {
display: block;
content: "Authors Notes:";
font-weight: bold;
line-height: 1;
}
}
article {
margin: 1rem 0;
font-size: 1.25rem;
line-height: 1.5;
p { margin-block-end: 1rem; }
}
</style>

24
src/layouts/Layout.astro Normal file
View File

@ -0,0 +1,24 @@
---
import "@/assets/styles/base.css";
interface Props {
title?: string;
}
const { title = "haetae" }: Props = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<meta name="pinterest" content="nopin nohover" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>

10
src/pages/about.md Normal file
View File

@ -0,0 +1,10 @@
---
title: about me
layout: ../layouts/About.astro
---
# hello!
it's me, the weirdo. here's a bunch of my images:
## badges...

View File

@ -0,0 +1,54 @@
---
import Blog from '@/layouts/Blog.astro';
import type { GetStaticPaths } from 'astro';
import { getCollection, render } from 'astro:content';
export const getStaticPaths = (async () => {
const blog = await getCollection("blog");
return blog.map(entry => ({
params: { id: entry.id },
props: { entry },
}));
}) satisfies GetStaticPaths;
const { entry } = Astro.props;
const { Content } = await render(entry);
const blog = await getCollection("blog");
blog.sort((a, b) => b.data.pubDate!.valueOf() - a.data.pubDate!.valueOf());
const current = blog.findIndex(entry => entry.id === Astro.params.id);
const previous = current + 1 === blog.length ? undefined : blog[current + 1];
const next = current === 0 ? undefined : blog[current - 1];
---
<Blog title={entry.data.title!} date={entry.data.pubDate!}>
<Content />
{(previous || next) && (
<div id="blog-pagination" slot="pagination">
{previous && (
<a id="previous" aria-label="previous post" href={`/blog/${previous.id}`}>{previous.data.title}</a>
)}
{next && (
<a id="next" aria-label="next post" href={`/blog/${next.id}`}>{next.data.title}</a>
)}
</div>
)}
</Blog>
<style>
#blog-pagination {
display: grid;
grid-template-columns: repeat(2, 1fr);
#previous {
grid-column: 1 / 2;
justify-self: left;
}
#next {
grid-column: 2 / -1;
justify-self: right;
}
}
</style>

View File

@ -0,0 +1,34 @@
---
import Layout from "@/layouts/Layout.astro";
import { getCollection } from "astro:content";
const blog = await getCollection("blog");
const sorted = Object.groupBy(blog, ({ data }) => data.pubDate!.getMonth());
const getMonth = (id: number) => {
let date = new Date();
date.setDate(1);
date.setMonth(id);
return date.toLocaleString(undefined, { month: "long" });
}
---
<Layout>
<h1>blog</h1>
<ul>
{Object.entries(sorted).map(entry => (
<li>
<h2>{getMonth(Number(entry[0]))}</h2>
<ul>
{entry[1]?.sort((a, b) => a.data.pubDate!.valueOf() - b.data.pubDate!.valueOf()).map(post => (
<li>
<time datetime={post.data.pubDate?.toISOString()}>
{post.data.pubDate?.toLocaleDateString(undefined, { dateStyle: "long" })}
</time>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</li>
))}
</ul>
</li>
))}
</ul>
</Layout>

23
src/pages/blog/rss.xml.ts Normal file
View File

@ -0,0 +1,23 @@
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
import rss from "@astrojs/rss";
import MarkdownIt from "markdown-it";
import sanitize from "sanitize-html";
const parser = new MarkdownIt();
export const GET: APIRoute = async (context) => {
const blog = await getCollection("blog");
return rss({
title: "haetae's blog",
description: "a blog about a weirdo who likes coding",
site: context.site!,
items: blog.map(entry => ({
link: `/blog/${entry.id}`,
content: sanitize(parser.render(entry.body!), {
allowedTags: sanitize.defaults.allowedTags.concat(["img"]),
}),
...entry.data,
})),
stylesheet: "/pretty-feed-v3.xsl",
});
}

View File

@ -0,0 +1,121 @@
---
import Chapter from "@/layouts/Chapter.astro";
import type { GetStaticPaths } from "astro";
import { getCollection, render } from "astro:content";
import { Breadcrumbs } from "astro-breadcrumbs";
export const getStaticPaths = (async () => {
const chapters = await getCollection("chapters");
return chapters.map(chapter => ({
params: {
ficId: chapter.id.split("/")[0],
chapterId: chapter.id.split("/")[1],
},
props: { chapter },
}));
}) satisfies GetStaticPaths;
const { ficId, chapterId } = Astro.params;
const { chapter } = Astro.props;
const { Content } = await render(chapter);
const chapters = await getCollection("chapters", ({ id }) => {
return id.split("/")[0] === ficId;
});
const fic = await getCollection("fics", ({ id }) => {
return 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];
const previous = current === 0 ? undefined : chapters[current - 1];
// lastmodified
const links = [
{ index: "last", text: chapter.data.title },
{ index: 2, text: fic[0].data.title },
];
---
<Chapter title={chapter.data.title} date={chapter.data.publishedAt} notes={chapter.data.notes}>
<Fragment slot="breadcrumbs">
<Breadcrumbs id="breadcrumbs" customizeLinks={links} linkTextFormat="capitalized">
<Fragment slot="separator" set:text="/" />
</Breadcrumbs>
</Fragment>
<Content />
<nav id="chapter-pagination" slot="pagination">
<div id="chapter-index">
<label for="chapter-select">Chapters:</label>
<select name="chapter-select" id="chapter-select">
{chapters.map(chapter => (
<option
value={`/fics/${chapter.id}`}
selected={chapter.id.split("/")[1] === chapterId ? "selected" : undefined}
>
{chapter.data.title}
</option>
))}
</select>
</div>
{previous && (
<a id="previous" href={`/fics/${previous.id}`}>{previous.data.title}</a>
)}
{next && (
<a id="next" href={`/fics/${next.id}`}>{next.data.title}</a>
)}
</nav>
</Chapter>
<style>
:global(#breadcrumbs) {
ol {
display: flex;
gap: 0.5rem;
list-style: none;
padding: 0;
li {
display: flex;
gap: 0.5rem;
}
.c-breadcrumbs__separator { opacity: 0.5; }
a[aria-current="location"] {
color: aqua;
}
}
}
#chapter-pagination {
display: grid;
grid: 1fr auto / repeat(2, 1fr);
gap: 0.5rem;
#chapter-index {
grid-area: 1 / 1 / 1 / -1;
width: min-content;
justify-self: center;
}
#previous {
grid-area: 2 / 1 / 2 / 1;
justify-self: left;
}
#next {
grid-area: 2 / 2 / 2 / 2;
justify-self: right;
}
}
</style>
<script>
const select: HTMLSelectElement | null = document.querySelector("#chapter-select");
select?.addEventListener("change", (e) => {
if (e.target instanceof HTMLSelectElement) {
window.location.href = e.target.value;
}
});
</script>

View File

@ -0,0 +1,42 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
import Layout from "@/layouts/Layout.astro";
export const getStaticPaths = (async () => {
const fics = await getCollection("fics");
return fics.map(fic => ({
params: { ficId: fic.id },
props: { fic },
}));
}) satisfies GetStaticPaths;
const { fic } = Astro.props;
const chapters = await getCollection("chapters", ({ id }) => {
return id.startsWith(fic.id);
});
chapters.length = Math.min(chapters.length, 5);
chapters.sort((a, b) => a.data.sortOrder - b.data.sortOrder);
---
<Layout>
<section>
<h1>{fic.data.title}</h1>
<header class="info">
<p>fandom: {fic.data.series.join(", ")}</p>
<time datetime={fic.data.publishedAt.toISOString()}>
{fic.data.publishedAt}
</time>
<blockquote>
<Fragment set:html={fic.data.summary.split("\n").join("<br />")} />
</blockquote>
<a href={`/fics/${chapters[0].id}`}>start reading</a>
<a href={`/fics/${Astro.params.ficId}/rss.xml`}>rss feed</a>
</header>
<h2>chapters</h2>
<ul>
{chapters.map(chapter => (
<li><a href={chapter.id}>{chapter.data.title}</a></li>
))}
</ul>
</section>
</Layout>

View File

@ -0,0 +1,36 @@
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
import rss from "@astrojs/rss";
import MarkdownIt from "markdown-it";
import sanitize from "sanitize-html";
const parser = new MarkdownIt();
const fics = await getCollection("fics");
export const GET: APIRoute = async (context) => {
const chapters = await getCollection("chapters", ({ id }) => {
return id.split("/")[0] === context.params.ficId;
});
const fic = fics.find(({ id }) => id === context.params.ficId);
return rss({
title: `${fic?.data.title}`,
description: `${fic?.data.summary}`,
site: context.site!,
items: chapters.map(chapter => ({
link: `/fics/${chapter.id}`,
title: chapter.data.title,
pubDate: chapter.data.publishedAt,
content: sanitize(parser.render(chapter.body!), {
allowedTags: sanitize.defaults.allowedTags.concat(["img"]),
}),
categories: fic?.data.series,
})),
stylesheet: "/pretty-feed-v3.xsl",
});
};
export function getStaticPaths() {
return fics.map(fic => ({
params: { ficId: fic.id },
}));
}

View File

@ -0,0 +1,32 @@
---
import Layout from "@/layouts/Layout.astro";
import { getCollection } from "astro:content";
const fics = await getCollection("fics");
const chapters = await getCollection("chapters");
chapters.length = Math.min(chapters.length, 5);
chapters.sort((a, b) => a.data.publishedAt.valueOf() - b.data.publishedAt.valueOf());
---
<Layout>
<h1>fanfics</h1>
<h2>recent updates</h2>
<ul>
{chapters.map(post => (
<li>
<time datetime={post.data.publishedAt.toISOString()}>
{post.data.publishedAt.toLocaleDateString(undefined, { dateStyle: "medium" })}
</time>
<a href={`/fics/${post.id}`}>{post.data.title}</a> in
<a href={`/fics/${post.id.split("/")[0]}`}>{fics.find(({ id }) => post.id.startsWith(id))?.data.title}</a>
</li>
))}
</ul>
<h2>works</h2>
<ul>
{fics.map(fic => (
<li><a href={`/fics/${fic.id}`}>{fic.data.title}</a></li>
))}
</ul>
</Layout>

9
src/pages/gallery.astro Normal file
View File

@ -0,0 +1,9 @@
---
import Layout from "@/layouts/Layout.astro";
---
<Layout>
<main>
<h1>gallery</h1>
import images here
</main>
</Layout>

59
src/pages/guestbook.astro Normal file
View File

@ -0,0 +1,59 @@
---
import { actions, isInputError } from "astro:actions";
import { db, Guestbook as table } from "astro:db";
import Layout from "@/layouts/Layout.astro";
export const prerender = false;
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());
---
<Layout>
<main>
<h1>Guestbook</h1>
<form action={actions.guestbook} method="post">
<label for="username">Username</label>
<input type="text" name="username" id="username" required aria-describedby="username-error" />
{errors.username && <p id="username-error">{errors.username.join(",")}</p>}
<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="body">Message</label>
<textarea name="body" id="body" rows="5" required aria-describedby="body-error"></textarea>
{errors.body && <p id="body-error">{errors.body.join(",")}</p>}
<input type="hidden" name="honeypot" tabindex="-1" autocomplete="off" style="display:none" />
<button type="submit">Post</button>
{errors.honeypot && <p>{errors.honeypot.join(",")}</p>}
</form>
{entries.map(({ username, website, body, date }) => (
<article class="entry">
<h1>{username}</h1>
{website && <p><a href={website} target="_blank" referrerpolicy="no-referrer">website</a></p>}
<time datetime={date.toISOString()}>{date}</time>
<div>
{body}
</div>
</article>
))}
</main>
</Layout>
<style>
form {
display: flex;
flex-flow: column wrap;
input, textarea {
margin-bottom: 1rem;
}
}
</style>

19
src/pages/index.astro Normal file
View File

@ -0,0 +1,19 @@
---
import Navbar from '~/Navbar.astro';
import Layout from '@/layouts/Layout.astro';
---
<Layout>
<Navbar />
<main>
<h1>hi</h1>
<p>this is a test chummy but cool &lt;===</p>
</main>
</Layout>
<style>
main {
max-width: clamp(50ch, 80ch, 100%);
margin: 0 auto;
}
</style>

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["src/components/*"],
}
}
}