Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07a4812596 | ||
|
|
51edf5f297 | ||
|
|
0fd27716af |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,106 @@
|
||||
<script lang="ts" module>
|
||||
export interface Book {
|
||||
title: string
|
||||
author?: string
|
||||
isbn: string
|
||||
status: 'reading' | 'finished'
|
||||
/** Resolved cover URL (Astro-optimized local image, or a remote fallback). */
|
||||
coverSrc: string
|
||||
/** Reading progress, 0–100. Only rendered for `status: 'reading'`. */
|
||||
progress?: number
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import IconBook from '~icons/lucide/book'
|
||||
|
||||
interface Props {
|
||||
book: Book
|
||||
/** 'cover' = vertical cover tile (grids); 'row' = horizontal card (teasers). */
|
||||
variant?: 'cover' | 'row'
|
||||
}
|
||||
|
||||
let { book, variant = 'cover' }: Props = $props()
|
||||
|
||||
let errored = $state(false)
|
||||
|
||||
const showProgress = $derived(
|
||||
book.status === 'reading' && book.progress != null,
|
||||
)
|
||||
</script>
|
||||
|
||||
{#snippet coverImage()}
|
||||
{#if errored}
|
||||
<div
|
||||
class="flex h-full w-full flex-col items-center justify-center gap-1 p-2 text-center"
|
||||
>
|
||||
<IconBook class="size-5 text-primary" />
|
||||
<span class="line-clamp-3 text-[10px] font-medium text-foreground">{book.title}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<img
|
||||
alt={`Cover of ${book.title}`}
|
||||
src={book.coverSrc}
|
||||
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
onerror={() => (errored = true)}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet progressBar()}
|
||||
{#if showProgress}
|
||||
<div class="mt-1.5 flex items-center gap-2">
|
||||
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-secondary">
|
||||
<div class="h-full rounded-full bg-primary" style={`width: ${book.progress}%`}></div>
|
||||
</div>
|
||||
<span class="text-[10px] font-medium tabular-nums text-muted-foreground">
|
||||
{book.progress}%
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if variant === 'row'}
|
||||
<a
|
||||
class="group flex h-full items-center gap-4 rounded-2xl border border-card-foreground/10 bg-card p-4 transition-all duration-300 hover:translate-y-[-4px] hover:shadow-lg"
|
||||
href={`https://openlibrary.org/isbn/${book.isbn}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${book.title}${book.author ? ` by ${book.author}` : ''}`}
|
||||
>
|
||||
<div
|
||||
class="aspect-2/3 w-14 shrink-0 overflow-hidden rounded-md border border-card-foreground/10 bg-secondary shadow-sm"
|
||||
>
|
||||
{@render coverImage()}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="line-clamp-2 text-sm font-medium text-foreground">{book.title}</h3>
|
||||
{#if book.author}
|
||||
<p class="truncate text-xs text-muted-foreground">{book.author}</p>
|
||||
{/if}
|
||||
{@render progressBar()}
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
class="group flex flex-col gap-y-2 transition-all duration-300 hover:translate-y-[-4px]"
|
||||
href={`https://openlibrary.org/isbn/${book.isbn}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${book.title}${book.author ? ` by ${book.author}` : ''}`}
|
||||
>
|
||||
<div
|
||||
class="aspect-2/3 w-full overflow-hidden rounded-lg border border-card-foreground/10 bg-secondary shadow-sm transition-shadow duration-300 group-hover:shadow-lg"
|
||||
>
|
||||
{@render coverImage()}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h3 class="line-clamp-2 text-sm font-medium text-foreground">{book.title}</h3>
|
||||
{#if book.author}
|
||||
<p class="truncate text-xs text-muted-foreground">{book.author}</p>
|
||||
{/if}
|
||||
{@render progressBar()}
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import BookCard, { type Book } from './BookCard.svelte'
|
||||
|
||||
interface Props {
|
||||
books: Book[]
|
||||
}
|
||||
|
||||
let { books }: Props = $props()
|
||||
|
||||
// Both lists render in the order they appear in reading.ts (curated by
|
||||
// relevance — most relevant first).
|
||||
const currentlyReading = $derived(books.filter((b) => b.status === 'reading'))
|
||||
const finished = $derived(books.filter((b) => b.status === 'finished'))
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-y-12">
|
||||
{#if currentlyReading.length > 0}
|
||||
<section aria-labelledby="currently-reading-title">
|
||||
<h3
|
||||
id="currently-reading-title"
|
||||
class="font-custom mb-6 text-xl font-bold text-foreground"
|
||||
>
|
||||
Currently Reading
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 gap-4 sm:grid-cols-4 md:grid-cols-5">
|
||||
{#each currentlyReading as book (book.isbn)}
|
||||
<BookCard {book} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if finished.length > 0}
|
||||
<section aria-labelledby="finished-title">
|
||||
<h3 id="finished-title" class="font-custom mb-6 text-xl font-bold text-foreground">
|
||||
Finished
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 gap-4 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6">
|
||||
{#each finished as book (book.isbn)}
|
||||
<BookCard {book} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -24,6 +24,10 @@ export const NAV_LINKS: SocialLink[] = [
|
||||
href: '/blog',
|
||||
label: 'blog',
|
||||
},
|
||||
{
|
||||
href: '/reading',
|
||||
label: 'reading',
|
||||
},
|
||||
{
|
||||
href: '/#contact',
|
||||
label: 'contact',
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import type { ImageMetadata } from 'astro'
|
||||
|
||||
import devopsHandbook from '@/assets/reading/devops-handbook.jpg'
|
||||
import osThreeEasyPieces from '@/assets/reading/os-three-easy-pieces.jpg'
|
||||
import cleanCode from '@/assets/reading/clean-code.jpg'
|
||||
import refactoring from '@/assets/reading/refactoring.jpg'
|
||||
import philosophyOfSoftwareDesign from '@/assets/reading/philosophy-of-software-design.jpg'
|
||||
import architecturePatternsWithPython from '@/assets/reading/architecture-patterns-with-python.jpg'
|
||||
import fluentPython from '@/assets/reading/fluent-python.jpg'
|
||||
import effectiveTypeScript from '@/assets/reading/effective-typescript.jpg'
|
||||
import headFirstDesignPatterns from '@/assets/reading/head-first-design-patterns.jpg'
|
||||
import ooThoughtProcess from '@/assets/reading/oo-thought-process.jpg'
|
||||
import dockerUpAndRunning from '@/assets/reading/docker-up-and-running.jpg'
|
||||
import theCProgrammingLanguage from '@/assets/reading/the-c-programming-language.jpg'
|
||||
import codePetzold from '@/assets/reading/code-petzold.jpg'
|
||||
import theoryOfComputation from '@/assets/reading/theory-of-computation.jpg'
|
||||
import headFirstSoftwareDevelopment from '@/assets/reading/head-first-software-development.jpg'
|
||||
import docsForDevelopers from '@/assets/reading/docs-for-developers.jpg'
|
||||
import justForFun from '@/assets/reading/just-for-fun.jpg'
|
||||
import algorithmsToLiveBy from '@/assets/reading/algorithms-to-live-by.jpg'
|
||||
import personalMba from '@/assets/reading/personal-mba.jpg'
|
||||
import fastAPI from '@/assets/reading/fastapi.jpg'
|
||||
|
||||
export interface ReadingEntry {
|
||||
title: string
|
||||
author?: string
|
||||
isbn: string
|
||||
status: 'reading' | 'finished'
|
||||
/**
|
||||
* Local cover image (optimized by Astro). When omitted, the cover falls back
|
||||
* to the Open Library Covers API by ISBN, and then to a title/author card if
|
||||
* that's unavailable too. To add a missing cover, drop a JPG in
|
||||
* src/assets/reading/, import it above, and set it here.
|
||||
*/
|
||||
cover?: ImageMetadata
|
||||
/** Reading progress, 0–100. Only shown for `status: 'reading'`. */
|
||||
progress?: number
|
||||
}
|
||||
|
||||
// Order matters: books render in the order listed here, curated by relevance
|
||||
// (most relevant to my work first).
|
||||
export const reading: ReadingEntry[] = [
|
||||
// Currently reading
|
||||
{
|
||||
title: 'The DevOps Handbook',
|
||||
author: 'Gene Kim',
|
||||
isbn: '9781942788003',
|
||||
status: 'reading',
|
||||
cover: devopsHandbook,
|
||||
progress: 55, // update as you read
|
||||
},
|
||||
{
|
||||
title: 'Operating Systems: Three Easy Pieces',
|
||||
author: 'Remzi H. Arpaci-Dusseau',
|
||||
isbn: '9781985086593',
|
||||
status: 'reading',
|
||||
cover: osThreeEasyPieces,
|
||||
progress: 30,
|
||||
},
|
||||
{
|
||||
title: 'Effective TypeScript',
|
||||
author: 'Dan Vanderkam',
|
||||
isbn: '9781492053743',
|
||||
status: 'reading',
|
||||
cover: effectiveTypeScript,
|
||||
progress: 45,
|
||||
},
|
||||
|
||||
// Finished — ordered by relevance
|
||||
{
|
||||
title: 'Clean Code',
|
||||
author: 'Robert C. Martin',
|
||||
isbn: '9780132350884',
|
||||
status: 'finished',
|
||||
cover: cleanCode,
|
||||
},
|
||||
{
|
||||
title: 'Refactoring',
|
||||
author: 'Martin Fowler',
|
||||
isbn: '9780134757599',
|
||||
status: 'finished',
|
||||
cover: refactoring,
|
||||
},
|
||||
{
|
||||
title: 'A Philosophy of Software Design',
|
||||
author: 'John Ousterhout',
|
||||
isbn: '9781732102200',
|
||||
status: 'finished',
|
||||
cover: philosophyOfSoftwareDesign,
|
||||
},
|
||||
{
|
||||
title: 'Architecture Patterns with Python',
|
||||
author: 'Harry Percival, Bob Gregory',
|
||||
isbn: '9781492052203',
|
||||
status: 'finished',
|
||||
cover: architecturePatternsWithPython,
|
||||
},
|
||||
{
|
||||
title: 'Fluent Python',
|
||||
author: 'Luciano Ramalho',
|
||||
isbn: '9781492056355',
|
||||
status: 'finished',
|
||||
cover: fluentPython,
|
||||
},
|
||||
{
|
||||
title: 'Head First Design Patterns',
|
||||
author: 'Eric Freeman',
|
||||
isbn: '9781492078005',
|
||||
status: 'finished',
|
||||
cover: headFirstDesignPatterns,
|
||||
},
|
||||
{
|
||||
title: 'The Object-Oriented Thought Process',
|
||||
author: 'Matt Weisfeld',
|
||||
isbn: '9780321861276',
|
||||
status: 'finished',
|
||||
cover: ooThoughtProcess,
|
||||
},
|
||||
{
|
||||
title: 'Docker: Up & Running',
|
||||
author: 'Sean Kane, Karl Matthias',
|
||||
isbn: '9781098131821',
|
||||
status: 'finished',
|
||||
cover: dockerUpAndRunning,
|
||||
},
|
||||
{
|
||||
title: 'FastAPI',
|
||||
author: 'Bill Lubanovic',
|
||||
isbn: '9781098135508',
|
||||
status: 'finished',
|
||||
cover: fastAPI,
|
||||
},
|
||||
{
|
||||
title: 'The C Programming Language',
|
||||
author: 'Brian Kernighan, Dennis Ritchie',
|
||||
isbn: '9780131103627',
|
||||
status: 'finished',
|
||||
cover: theCProgrammingLanguage,
|
||||
},
|
||||
{
|
||||
title: 'Code: The Hidden Language of Computer Hardware and Software',
|
||||
author: 'Charles Petzold',
|
||||
isbn: '9780137909100',
|
||||
status: 'finished',
|
||||
cover: codePetzold,
|
||||
},
|
||||
{
|
||||
title: 'Introduction to the Theory of Computation',
|
||||
author: 'Michael Sipser',
|
||||
isbn: '9781133187790',
|
||||
status: 'finished',
|
||||
cover: theoryOfComputation,
|
||||
},
|
||||
{
|
||||
title: 'Head First Software Development',
|
||||
author: 'Dan Pilone',
|
||||
isbn: '9780596527358',
|
||||
status: 'finished',
|
||||
cover: headFirstSoftwareDevelopment,
|
||||
},
|
||||
{
|
||||
title: 'Docs for Developers',
|
||||
author: 'Jared Bhatti',
|
||||
isbn: '9781484272169',
|
||||
status: 'finished',
|
||||
cover: docsForDevelopers,
|
||||
},
|
||||
{
|
||||
title: 'Just for Fun',
|
||||
author: 'Linus Torvalds, David Diamond',
|
||||
isbn: '9780066620732',
|
||||
status: 'finished',
|
||||
cover: justForFun,
|
||||
},
|
||||
{
|
||||
title: 'Algorithms to Live By',
|
||||
author: 'Brian Christian, Tom Griffiths',
|
||||
isbn: '9781627790369',
|
||||
status: 'finished',
|
||||
cover: algorithmsToLiveBy,
|
||||
},
|
||||
{
|
||||
title: 'The Personal MBA',
|
||||
author: 'Josh Kaufman',
|
||||
isbn: '9781591845577',
|
||||
status: 'finished',
|
||||
cover: personalMba,
|
||||
},
|
||||
]
|
||||
@@ -1,4 +1,7 @@
|
||||
import { getCollection, type CollectionEntry } from 'astro:content'
|
||||
import { getImage } from 'astro:assets'
|
||||
import { reading } from '@/data/reading'
|
||||
import type { Book } from '@/components/svelte/BookCard.svelte'
|
||||
|
||||
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
|
||||
const posts = await getCollection('blog')
|
||||
@@ -92,6 +95,23 @@ export async function getFeaturedProjects(): Promise<CollectionEntry<'projects'>
|
||||
return projects.slice(0, 4)
|
||||
}
|
||||
|
||||
export async function getReadingList(): Promise<Book[]> {
|
||||
return Promise.all(
|
||||
reading.map(async (entry) => ({
|
||||
title: entry.title,
|
||||
author: entry.author,
|
||||
isbn: entry.isbn,
|
||||
status: entry.status,
|
||||
progress: entry.progress,
|
||||
// Local covers get Astro-optimized to WebP; everything else falls back to
|
||||
// the Open Library Covers API by ISBN (and then a title/author card).
|
||||
coverSrc: entry.cover
|
||||
? (await getImage({ src: entry.cover, width: 400, format: 'webp' })).src
|
||||
: `https://covers.openlibrary.org/b/isbn/${entry.isbn}-L.jpg?default=false`,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
export async function getProjectsFeaturedTags(maxCount: number): Promise<string[]> {
|
||||
const projects = await getAllProjects()
|
||||
const tags = new Set<string>()
|
||||
|
||||
@@ -6,7 +6,7 @@ import Skills from '../components/svelte/Skills.svelte'
|
||||
import { buttonVariants } from '@/lib/button-variants'
|
||||
import Logo from '../components/svelte/Logo.svelte'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { getRecentPosts, getFeaturedProjects } from '@/lib/data-utils'
|
||||
import { getRecentPosts, getFeaturedProjects, getReadingList } from '@/lib/data-utils'
|
||||
import { SITE } from '@/consts'
|
||||
import ProjectCard from '../components/svelte/ProjectCard.svelte'
|
||||
import IconGit from '~icons/simple-icons/git'
|
||||
@@ -19,9 +19,14 @@ import EducationItem from '../components/svelte/EducationItem.svelte'
|
||||
import { education } from '@/data/education'
|
||||
import ContactForm from '../components/svelte/ContactForm.svelte'
|
||||
import IconMail from '~icons/lucide/mail'
|
||||
import IconBookOpen from '~icons/lucide/book-open'
|
||||
import BookCard from '../components/svelte/BookCard.svelte'
|
||||
|
||||
const blog = await getRecentPosts(3)
|
||||
const featuredProjects = await getFeaturedProjects()
|
||||
const currentlyReading = (await getReadingList()).filter(
|
||||
(book) => book.status === 'reading',
|
||||
)
|
||||
const currentUrl = Astro.url;
|
||||
const turnstileSitekey = import.meta.env.PUBLIC_TURNSTILE_SITEKEY
|
||||
---
|
||||
@@ -157,6 +162,39 @@ const turnstileSitekey = import.meta.env.PUBLIC_TURNSTILE_SITEKEY
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{currentlyReading.length > 0 && (
|
||||
<section class="flex flex-col gap-y-4" aria-labelledby="reading-title" role="region">
|
||||
<div class="relative">
|
||||
<h2
|
||||
id="reading-title"
|
||||
class="font-custom text-foreground text-2xl font-bold flex items-center gap-2"
|
||||
>
|
||||
<IconBookOpen class="size-5" />
|
||||
Currently Reading
|
||||
</h2>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
What's on my desk right now.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
|
||||
{currentlyReading.map((book) => (
|
||||
<BookCard book={book} variant="row" client:load />
|
||||
))}
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<Link
|
||||
href="/reading"
|
||||
class={buttonVariants({ variant: 'ghost' }) + ' group'}
|
||||
>
|
||||
View full reading list <span
|
||||
class="ml-1.5 transition-transform group-hover:translate-x-1"
|
||||
>→</span
|
||||
>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section class="flex flex-col gap-y-4" aria-labelledby="education-title" role="region">
|
||||
<div class="relative">
|
||||
<h2
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||
import PageHead from '@/components/PageHead.astro'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import ReadingList from '@/components/svelte/ReadingList.svelte'
|
||||
import { getReadingList } from '@/lib/data-utils'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
|
||||
const currentUrl = Astro.url
|
||||
const books = await getReadingList()
|
||||
---
|
||||
|
||||
<Layout canonicalUrl={currentUrl}>
|
||||
<PageHead slot="head" title="Reading List" />
|
||||
<Breadcrumbs
|
||||
items={[{ label: 'Reading', href: '/reading', icon: 'lucide:book-open' }]}
|
||||
/>
|
||||
|
||||
<section class="max-screen mt-12 px-4 md:px-6">
|
||||
<div class="mb-12">
|
||||
<div class="flex w-fit items-center gap-2 text-primary">
|
||||
<Icon name="lucide:book-open" class="h-4 w-4 text-primary" />
|
||||
<p class="word-spacing font-mono text-sm uppercase leading-none text-primary">
|
||||
What I'm reading
|
||||
</p>
|
||||
</div>
|
||||
<h2 class="font-custom text-foreground mt-2 text-4xl font-bold">Reading List</h2>
|
||||
<p class="text-muted-foreground text-md mt-3 max-w-2xl">
|
||||
Books I'm working through and ones I've finished — mostly software
|
||||
engineering and computer science, with the occasional detour.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-16">
|
||||
<ReadingList books={books} client:load />
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -2,7 +2,6 @@
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||