feat: add reading list section
Generate a build and push to Cloudflare Pages / Build and Deploy to Cloudflare Pages (push) Successful in 3m47s
|
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',
|
href: '/blog',
|
||||||
label: 'blog',
|
label: 'blog',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/reading',
|
||||||
|
label: 'reading',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/#contact',
|
href: '/#contact',
|
||||||
label: '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 { 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'>[]> {
|
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
|
||||||
const posts = await getCollection('blog')
|
const posts = await getCollection('blog')
|
||||||
@@ -92,6 +95,23 @@ export async function getFeaturedProjects(): Promise<CollectionEntry<'projects'>
|
|||||||
return projects.slice(0, 4)
|
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[]> {
|
export async function getProjectsFeaturedTags(maxCount: number): Promise<string[]> {
|
||||||
const projects = await getAllProjects()
|
const projects = await getAllProjects()
|
||||||
const tags = new Set<string>()
|
const tags = new Set<string>()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Skills from '../components/svelte/Skills.svelte'
|
|||||||
import { buttonVariants } from '@/lib/button-variants'
|
import { buttonVariants } from '@/lib/button-variants'
|
||||||
import Logo from '../components/svelte/Logo.svelte'
|
import Logo from '../components/svelte/Logo.svelte'
|
||||||
import Layout from '@/layouts/Layout.astro'
|
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 { SITE } from '@/consts'
|
||||||
import ProjectCard from '../components/svelte/ProjectCard.svelte'
|
import ProjectCard from '../components/svelte/ProjectCard.svelte'
|
||||||
import IconGit from '~icons/simple-icons/git'
|
import IconGit from '~icons/simple-icons/git'
|
||||||
@@ -19,9 +19,14 @@ import EducationItem from '../components/svelte/EducationItem.svelte'
|
|||||||
import { education } from '@/data/education'
|
import { education } from '@/data/education'
|
||||||
import ContactForm from '../components/svelte/ContactForm.svelte'
|
import ContactForm from '../components/svelte/ContactForm.svelte'
|
||||||
import IconMail from '~icons/lucide/mail'
|
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 blog = await getRecentPosts(3)
|
||||||
const featuredProjects = await getFeaturedProjects()
|
const featuredProjects = await getFeaturedProjects()
|
||||||
|
const currentlyReading = (await getReadingList()).filter(
|
||||||
|
(book) => book.status === 'reading',
|
||||||
|
)
|
||||||
const currentUrl = Astro.url;
|
const currentUrl = Astro.url;
|
||||||
const turnstileSitekey = import.meta.env.PUBLIC_TURNSTILE_SITEKEY
|
const turnstileSitekey = import.meta.env.PUBLIC_TURNSTILE_SITEKEY
|
||||||
---
|
---
|
||||||
@@ -157,6 +162,39 @@ const turnstileSitekey = import.meta.env.PUBLIC_TURNSTILE_SITEKEY
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="flex flex-col gap-y-4" aria-labelledby="education-title" role="region">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<h2
|
<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>
|
||||||