diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/ReadingEntry.astro | 62 | ||||
| -rw-r--r-- | src/content/config.ts | 21 | ||||
| -rw-r--r-- | src/content/reading.yaml | 80 | ||||
| -rw-r--r-- | src/layouts/BaseLayout.astro | 1 | ||||
| -rw-r--r-- | src/pages/reading.astro | 60 | ||||
| -rw-r--r-- | src/styles/sitemap.css | 133 |
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; + } +} |
