summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMatthew Kosarek <matthew@matthewkosarek.xyz>2026-08-09 19:32:06 -0400
committerMatthew Kosarek <matthew@matthewkosarek.xyz>2026-08-09 19:32:06 -0400
commitedd3b0972115ebddd068ed226158ace07c706401 (patch)
treeeca3a2ffc85283a99bbce1e7b0068ff5caa78f58 /src
parente51d273eff923a2c0c691d6f1d740bde74d05b56 (diff)
feature: add a reading pageHEADmaster
Diffstat (limited to 'src')
-rw-r--r--src/components/ReadingEntry.astro62
-rw-r--r--src/content/config.ts21
-rw-r--r--src/content/reading.yaml80
-rw-r--r--src/layouts/BaseLayout.astro1
-rw-r--r--src/pages/reading.astro60
-rw-r--r--src/styles/sitemap.css133
6 files changed, 356 insertions, 1 deletions
diff --git a/src/components/ReadingEntry.astro b/src/components/ReadingEntry.astro
new file mode 100644
index 0000000..3fa7860
--- /dev/null
+++ b/src/components/ReadingEntry.astro
@@ -0,0 +1,62 @@
+---
+import type { CollectionEntry } from "astro:content";
+
+type ReadingYear = CollectionEntry<"reading">;
+type Entry = ReadingYear["data"]["entries"][number];
+
+interface Props {
+ entry: Entry;
+}
+
+const { entry } = Astro.props;
+
+const href =
+ entry.url ??
+ (entry.isbn ? `https://openlibrary.org/isbn/${entry.isbn}` : undefined);
+const isReading = entry.status === "reading";
+const statusLabel = isReading ? "Currently reading" : "Finished";
+---
+
+<li>
+ <p>
+ <span
+ class:list={[
+ "reading-status",
+ isReading ? "reading-status--reading" : "reading-status--done",
+ ]}
+ role="img"
+ aria-label={statusLabel}
+ title={statusLabel}
+ >
+ {
+ !isReading && (
+ <svg viewBox="0 0 16 16" aria-hidden="true">
+ <path d="M3.6 8.4 6.4 11.2 12.4 4.8" />
+ </svg>
+ )
+ }
+ </span>
+ {
+ href ? (
+ <a href={href} target="_blank" rel="noopener noreferrer">{entry.title}</a>
+ ) : (
+ <span class="reading-title">{entry.title}</span>
+ )
+ }
+ {entry.author && <span class="reading-author">by {entry.author}</span>}
+ </p>
+ {entry.note && <div class="reading-note">{entry.note}</div>}
+ {
+ entry.isbn && (
+ <div class="reading-isbn">
+ <a
+ href={`https://openlibrary.org/isbn/${entry.isbn}`}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ ISBN {entry.isbn}
+ </a>
+ </div>
+ )
+ }
+</li>
diff --git a/src/content/config.ts b/src/content/config.ts
index d8c4bec..74e6b89 100644
--- a/src/content/config.ts
+++ b/src/content/config.ts
@@ -1,4 +1,5 @@
import { defineCollection, z } from 'astro:content';
+import { file } from 'astro/loaders';
const posts = defineCollection({
type: 'content',
@@ -9,4 +10,22 @@ const posts = defineCollection({
}),
});
-export const collections = { posts };
+const readingEntry = z.object({
+ title: z.string(),
+ author: z.string().optional(),
+ type: z.enum(['book', 'article']).default('book'),
+ status: z.enum(['done', 'reading']).default('done'),
+ isbn: z.string().optional(),
+ url: z.string().url().optional(),
+ note: z.string().optional(),
+});
+
+const reading = defineCollection({
+ loader: file('src/content/reading.yaml'),
+ schema: z.object({
+ year: z.number(),
+ entries: z.array(readingEntry),
+ }),
+});
+
+export const collections = { posts, reading };
diff --git a/src/content/reading.yaml b/src/content/reading.yaml
new file mode 100644
index 0000000..bbd19cd
--- /dev/null
+++ b/src/content/reading.yaml
@@ -0,0 +1,80 @@
+# Reading list, rendered by src/pages/reading.astro
+#
+# Add a new year by adding a top-level key. Within a year, append entries in the
+# order you read them (oldest first) -- the page reverses them so the most
+# recent shows up at the top.
+#
+# Per-entry fields:
+# title (required)
+# author
+# type book | article (default: book)
+# status done | reading | planned (default: done)
+# isbn ISBN-10 or ISBN-13, links to https://openlibrary.org/isbn/<isbn>
+# url for articles, or to override the Open Library link
+# note optional one-liner shown under the title
+
+"2026":
+ year: 2026
+ entries:
+ - title: A Naturalist Along the Jersey Shore
+ author: Joanna Burger
+ isbn: "0813522994"
+
+ - title: The Lovely Bones
+ author: Alice Sebold
+ isbn: "9780316666343"
+
+ - title: The Odyssey
+ author: Homer
+ isbn: "9780393089059"
+ note: Translated by Emily Wilson
+
+ - title: "Sekret Machines Book 1: Chasing Shadows"
+ author: Tom DeLonge and A. J. Hartley
+ isbn: "9781943272150"
+
+ - title: Gulliver's Travels
+ author: Jonathan Swift
+ isbn: "9780141439495"
+
+ - title: SPQR
+ author: Mary Beard
+ isbn: "9780871404237"
+ status: reading
+
+ - title: Annihilation
+ author: Jeff VanderMeer
+ isbn: "9780374104092"
+ status: reading
+
+ - title: '"No Graphics API"'
+ author: Sebastian Aaltonen
+ type: article
+ url: https://www.sebastianaaltonen.com/blog/no-graphics-api
+
+"2025":
+ year: 2025
+ entries:
+ - title: "Gödel, Escher, Bach: An Eternal Golden Braid"
+ author: Douglas Hofstadter
+ isbn: "9780465026562"
+
+ - title: The Da Vinci Code
+ author: Dan Brown
+ isbn: "9780385504201"
+
+ - title: Wild
+ author: Cheryl Strayed
+ isbn: "9780307592736"
+
+ - title: The Republic
+ author: Plato
+ isbn: "9780140455113"
+
+ - title: Independent People
+ author: Halldór Laxness
+ isbn: "9780679767923"
+
+ - title: The Overstory
+ author: Richard Powers
+ isbn: "9780393635522"
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index 61bc2dc..24a6d55 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -50,6 +50,7 @@ const {
<li><a href='/resume'><OpenMoji code="1F4D8" alt="cv" /> CV</a></li>
<li><a href='/posts'><OpenMoji code="1F4DD" alt="posts" /> Posts</a></li>
<li><a href='/tech'><OpenMoji code="1F4BB" alt="laptop" /> Tech</a></li>
+ <li><a href='/reading'><OpenMoji code="1F4DA" alt="books" /> Reading</a></li>
<li style="margin-left: auto"><button id="theme_button_default"><OpenMoji code="1F642" alt="smiley" /></button></li>
<li><button id="theme_button_autumn"><OpenMoji code="1F341" alt="maple leaf" /></button></li>
<li><button id="theme_button_winter"><OpenMoji code="26C4" alt="snowman" /></button></li>
diff --git a/src/pages/reading.astro b/src/pages/reading.astro
new file mode 100644
index 0000000..0e1d1cb
--- /dev/null
+++ b/src/pages/reading.astro
@@ -0,0 +1,60 @@
+---
+import { getCollection, type CollectionEntry } from "astro:content";
+import BaseLayout from "../layouts/BaseLayout.astro";
+import ReadingEntry from "../components/ReadingEntry.astro";
+import "../styles/post.css";
+import "../styles/sitemap.css";
+
+type ReadingYear = CollectionEntry<"reading">;
+type Entry = ReadingYear["data"]["entries"][number];
+
+const years = await getCollection("reading");
+years.sort((a: ReadingYear, b: ReadingYear) => b.data.year - a.data.year);
+
+// Entries are authored oldest-first within a year; show the most recent first.
+function newestFirst(entries: Entry[]): Entry[] {
+ return [...entries].reverse();
+}
+---
+
+<BaseLayout
+ title="Reading"
+ description="Books and articles I've read/am reading"
+>
+ <div id="content" class="content">
+ {
+ years.map((year: ReadingYear) => {
+ const entries = newestFirst(year.data.entries);
+ const books = entries.filter((e: Entry) => e.type === "book");
+ const articles = entries.filter(
+ (e: Entry) => e.type === "article",
+ );
+
+ return (
+ <section class="reading-year">
+ <h2>{year.data.year}</h2>
+
+ <div class="reading-year-body">
+ <ul class="org-ul">
+ {books.map((entry: Entry) => (
+ <ReadingEntry entry={entry} />
+ ))}
+ </ul>
+
+ {articles.length > 0 && (
+ <Fragment>
+ <h3 class="reading-subhead">Articles</h3>
+ <ul class="org-ul">
+ {articles.map((entry: Entry) => (
+ <ReadingEntry entry={entry} />
+ ))}
+ </ul>
+ </Fragment>
+ )}
+ </div>
+ </section>
+ );
+ })
+ }
+ </div>
+</BaseLayout>
diff --git a/src/styles/sitemap.css b/src/styles/sitemap.css
index 316de00..f23d8af 100644
--- a/src/styles/sitemap.css
+++ b/src/styles/sitemap.css
@@ -134,3 +134,136 @@
.org-ul > li > .sitemap_date + .sitemap_tag {
margin-left: 0.75rem;
}
+
+/* --- Reading list (src/pages/reading.astro) --- */
+
+.reading-year {
+ margin-bottom: 2.5rem;
+}
+
+.reading-year > h2 {
+ margin-bottom: 1rem;
+}
+
+.reading-year-body {
+ padding-left: 1.5rem;
+}
+
+/* The shared .org-ul > li > p is 0.5rem, so vertical-align can't center the
+ status dot against the 1rem title. Flex the row instead. */
+.reading-year-body .org-ul > li > p {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.reading-year-body .org-ul > li > p > a {
+ min-width: 0;
+}
+
+.reading-subhead {
+ margin-top: 1.5rem;
+ margin-bottom: 1rem;
+ opacity: 0.8;
+}
+
+/* .org-ul > li > p shrinks to 0.5rem so the anchor can set its own size;
+ these siblings have to restate theirs. */
+.reading-title {
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+.reading-author {
+ font-size: 0.9rem;
+ font-style: italic;
+ opacity: 0.75;
+ margin-left: 0.4rem;
+}
+
+.reading-note {
+ width: 100%;
+ font-size: 0.8rem;
+ font-style: italic;
+ color: var(--color);
+ opacity: 0.6;
+}
+
+.reading-isbn {
+ width: 100%;
+ font-size: 0.75rem;
+ opacity: 0.55;
+}
+
+.reading-isbn a {
+ color: var(--color);
+ text-decoration: none;
+ font-family: monospace;
+}
+
+.reading-isbn a:hover {
+ text-decoration: underline;
+ opacity: 1;
+}
+
+/* Status dot sitting to the left of the title. Sized in rem because the
+ enclosing .org-ul > li > p drops to 0.5rem. */
+.reading-status {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ flex: 0 0 auto;
+ width: 0.9rem;
+ height: 0.9rem;
+ border-radius: 50%;
+ margin-right: 0.5rem;
+ vertical-align: middle;
+}
+
+.reading-status--done {
+ background: #3fa34d;
+ opacity: 0.8;
+}
+
+.reading-status--done svg {
+ width: 70%;
+ height: 70%;
+ fill: none;
+ stroke: #fff;
+ stroke-width: 2.4;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+/* "Recording" indicator: a slow pulse plus an expanding halo. */
+.reading-status--reading {
+ background: #e2543a;
+ animation: reading-blink 2.4s ease-in-out infinite;
+}
+
+.reading-status--reading::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ animation: reading-halo 2.4s ease-out infinite;
+}
+
+@keyframes reading-blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.4; }
+}
+
+@keyframes reading-halo {
+ 0% { box-shadow: 0 0 0 0 rgba(226, 84, 58, 0.5); }
+ 70% { box-shadow: 0 0 0 0.45rem rgba(226, 84, 58, 0); }
+ 100% { box-shadow: 0 0 0 0 rgba(226, 84, 58, 0); }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .reading-status--reading,
+ .reading-status--reading::after {
+ animation: none;
+ }
+}