diff --git a/public/avatar.png b/public/avatar.png new file mode 100644 index 0000000..a1c2ac6 Binary files /dev/null and b/public/avatar.png differ diff --git a/src/pages/experience/[...slug].astro b/src/pages/experience/[...slug].astro new file mode 100644 index 0000000..e81e40e --- /dev/null +++ b/src/pages/experience/[...slug].astro @@ -0,0 +1,110 @@ +--- +import { render } from "astro:content"; +import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; +import { Icon } from "astro-icon/components"; +import PostPreview from "@/components/blog/PostPreview.astro"; +import { getAllExperience, getContentForExperience } from "@/data/experience"; +import PageLayout from "@/layouts/Base.astro"; + +export const getStaticPaths = (async () => { + const experiences = await getAllExperience(); + return Promise.all( + experiences.map(async (exp) => ({ + params: { slug: exp.id }, + props: { exp, content: await getContentForExperience(exp.id) }, + })), + ); +}) satisfies GetStaticPaths; + +type Props = InferGetStaticPropsType; + +const { exp, content } = Astro.props as Props; +const { Content } = await render(exp); +const { startDate, endDate, organization, role, location, url } = exp.data; +const range = `${startDate.getFullYear()} – ${endDate ? endDate.getFullYear() : "Present"}`; + +const meta = { + description: exp.data.description ?? `${role ?? ""} at ${organization}`.trim(), + title: exp.data.title, +}; +--- + + + + +

{role ? `${role}, ${organization}` : organization}

+

+ {range}{location && ` · ${location}`} + { + url && ( + <> + {" · "} + + Website → + + + ) + } +

+ +
+ +
+ + { + content.projects.length > 0 && ( +
+

Projects

+ +
+ ) + } + + { + content.writing.length > 0 && ( +
+

Writing

+
    + {content.writing.map((p) => ( +
  • + +
  • + ))} +
+
+ ) + } + + { + content.speaking.length > 0 && ( +
+

Speaking

+ +
+ ) + } +
diff --git a/src/pages/experience/index.astro b/src/pages/experience/index.astro new file mode 100644 index 0000000..a9ae52e --- /dev/null +++ b/src/pages/experience/index.astro @@ -0,0 +1,36 @@ +--- +import { getAllExperience } from "@/data/experience"; +import PageLayout from "@/layouts/Base.astro"; + +const experiences = await getAllExperience(); + +const meta = { + description: "Roles and organizations I've worked with", + title: "Experience", +}; + +const range = (start: Date, end?: Date) => + `${start.getFullYear()} – ${end ? end.getFullYear() : "Present"}`; +--- + + +

Experience

+ +
diff --git a/src/pages/projects/[...page].astro b/src/pages/projects/[...page].astro new file mode 100644 index 0000000..5c1f7ae --- /dev/null +++ b/src/pages/projects/[...page].astro @@ -0,0 +1,62 @@ +--- +import { type CollectionEntry, getCollection } from "astro:content"; +import type { GetStaticPaths, Page } from "astro"; +import { Icon } from "astro-icon/components"; +import Pagination from "@/components/Paginator.astro"; +import Project from "@/components/project/Project.astro"; +import PageLayout from "@/layouts/Base.astro"; +import { collectionDateSort } from "@/utils/date"; + +export const getStaticPaths = (async ({ paginate }) => { + const MAX_PROJECTS_PER_PAGE = 10; + const allProjects = await getCollection("projects"); + return paginate(allProjects.sort(collectionDateSort), { pageSize: MAX_PROJECTS_PER_PAGE }); +}) satisfies GetStaticPaths; + +interface Props { + page: Page>; +} + +const { page } = Astro.props; + +const meta = { + description: "Things I've built", + title: "Projects", +}; + +const paginationProps = { + ...(page.url.prev && { + prevUrl: { + text: "← Previous Page", + url: page.url.prev, + }, + }), + ...(page.url.next && { + nextUrl: { + text: "Next Page →", + url: page.url.next, + }, + }), +}; +--- + + +
+

+ Projects + RSS feed + +

+
    + { + page.data.map((project) => ( +
  • + +
  • + )) + } +
+ +
+
diff --git a/src/pages/projects/[...slug].astro b/src/pages/projects/[...slug].astro new file mode 100644 index 0000000..bb0c2a8 --- /dev/null +++ b/src/pages/projects/[...slug].astro @@ -0,0 +1,28 @@ +--- +import { getCollection } from "astro:content"; +import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; +import Project from "@/components/project/Project.astro"; +import PageLayout from "@/layouts/Base.astro"; + +// if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr +export const getStaticPaths = (async () => { + const allProjects = await getCollection("projects"); + return allProjects.map((project) => ({ + params: { slug: project.id }, + props: { project }, + })); +}) satisfies GetStaticPaths; + +export type Props = InferGetStaticPropsType; + +const { project } = Astro.props; + +const meta = { + description: project.data.description || `Project: ${project.data.title}`, + title: project.data.title, +}; +--- + + + + diff --git a/src/pages/projects/rss.xml.ts b/src/pages/projects/rss.xml.ts new file mode 100644 index 0000000..7a9e04d --- /dev/null +++ b/src/pages/projects/rss.xml.ts @@ -0,0 +1,18 @@ +import { getCollection } from "astro:content"; +import rss from "@astrojs/rss"; +import { siteConfig } from "@/site.config"; + +export const GET = async () => { + const projects = await getCollection("projects"); + + return rss({ + title: siteConfig.title, + description: siteConfig.description, + site: import.meta.env.SITE, + items: projects.map((project) => ({ + title: project.data.title, + pubDate: project.data.publishDate, + link: `projects/${project.id}/`, + })), + }); +}; diff --git a/src/pages/speaking/[...page].astro b/src/pages/speaking/[...page].astro new file mode 100644 index 0000000..2bcc004 --- /dev/null +++ b/src/pages/speaking/[...page].astro @@ -0,0 +1,52 @@ +--- +import type { CollectionEntry } from "astro:content"; +import type { GetStaticPaths, Page } from "astro"; +import FormattedDate from "@/components/FormattedDate.astro"; +import Pagination from "@/components/Paginator.astro"; +import { getAllSpeaking } from "@/data/speaking"; +import PageLayout from "@/layouts/Base.astro"; +import { collectionDateSort } from "@/utils/date"; + +export const getStaticPaths = (async ({ paginate }) => { + const talks = await getAllSpeaking(); + return paginate(talks.sort(collectionDateSort), { pageSize: 10 }); +}) satisfies GetStaticPaths; + +interface Props { + page: Page>; +} + +const { page } = Astro.props; + +const meta = { + description: "Talks and presentations I've given", + title: "Speaking", +}; + +const paginationProps = { + ...(page.url.prev && { prevUrl: { text: "← Previous Page", url: page.url.prev } }), + ...(page.url.next && { nextUrl: { text: "Next Page →", url: page.url.next } }), +}; +--- + + +

Speaking

+
    + { + page.data.map((talk) => ( +
  • + +
    +

    + + {talk.data.title} + +

    + {talk.data.event &&

    {talk.data.event}

    } +
    +
  • + )) + } +
+ +
diff --git a/src/pages/speaking/[...slug].astro b/src/pages/speaking/[...slug].astro new file mode 100644 index 0000000..568640e --- /dev/null +++ b/src/pages/speaking/[...slug].astro @@ -0,0 +1,47 @@ +--- +import { getCollection, render } from "astro:content"; +import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; +import ExperienceTags from "@/components/ExperienceTags.astro"; +import FormattedDate from "@/components/FormattedDate.astro"; +import StreamPlayer from "@/components/StreamPlayer.astro"; +import PageLayout from "@/layouts/Base.astro"; + +export const getStaticPaths = (async () => { + const talks = await getCollection("speaking"); + return talks.map((talk) => ({ + params: { slug: talk.id }, + props: { talk }, + })); +}) satisfies GetStaticPaths; + +type Props = InferGetStaticPropsType; + +const { talk } = Astro.props as Props; +const { Content } = await render(talk); +const { title, description, videoId, originalSource, event, publishDate, experiences } = talk.data; + +const meta = { description, title }; +--- + + + + diff --git a/src/pages/writing/[...page].astro b/src/pages/writing/[...page].astro new file mode 100644 index 0000000..741fe03 --- /dev/null +++ b/src/pages/writing/[...page].astro @@ -0,0 +1,127 @@ +--- +import type { CollectionEntry } from "astro:content"; +import type { GetStaticPaths, Page } from "astro"; +import { Icon } from "astro-icon/components"; +import PostPreview from "@/components/blog/PostPreview.astro"; +import Pagination from "@/components/Paginator.astro"; +import { getAllExperience } from "@/data/experience"; +import { getAllPosts, groupPostsByYear } from "@/data/post"; +import PageLayout from "@/layouts/Base.astro"; +import { collectionDateSort } from "@/utils/date"; + +export const getStaticPaths = (async ({ paginate }) => { + const MAX_POSTS_PER_PAGE = 10; + const MAX_PINNED_POSTS = 3; + const allPosts = await getAllPosts(); + const allPostsByDate = allPosts.sort(collectionDateSort); + const experiences = await getAllExperience(); + const pinnedPosts = allPostsByDate + .values() + .filter((p) => p.data.pinned) + .take(MAX_PINNED_POSTS) + .toArray(); + return paginate(allPostsByDate, { + pageSize: MAX_POSTS_PER_PAGE, + props: { experiences, pinnedPosts }, + }); +}) satisfies GetStaticPaths; + +interface Props { + page: Page>; + experiences: CollectionEntry<"experience">[]; + pinnedPosts: CollectionEntry<"writing">[]; +} + +const { page, experiences, pinnedPosts } = Astro.props; + +const meta = { + description: "Essays, notes and articles I've written", + title: "Writing", +}; + +const paginationProps = { + ...(page.url.prev && { + prevUrl: { + text: "← Previous Page", + url: page.url.prev, + }, + }), + ...(page.url.next && { + nextUrl: { + text: "Next Page →", + url: page.url.next, + }, + }), +}; + +const groupedByYear = groupPostsByYear(page.data); +const descYearKeys = Object.keys(groupedByYear).sort((a, b) => +b - +a); +--- + + +
+

Writing

+ + RSS feed + +
+
+
+ { + pinnedPosts.length > 0 && ( +
+

Pinned

+
    + {pinnedPosts.map((p) => ( +
  • + +
  • + ))} +
+
+ ) + } + { + descYearKeys.map((yearKey) => ( +
+

+ Posts in + {yearKey} +

+
    + {groupedByYear[yearKey]?.map((p) => ( +
  • + +
  • + ))} +
+
+ )) + } + +
+ { + !!experiences.length && ( + + ) + } +
+
diff --git a/src/pages/writing/[...slug].astro b/src/pages/writing/[...slug].astro new file mode 100644 index 0000000..02047bd --- /dev/null +++ b/src/pages/writing/[...slug].astro @@ -0,0 +1,24 @@ +--- +import { render } from "astro:content"; +import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; +import { getAllPosts } from "@/data/post"; +import PostLayout from "@/layouts/BlogPost.astro"; + +// if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr +export const getStaticPaths = (async () => { + const blogEntries = await getAllPosts(); + return blogEntries.map((post) => ({ + params: { slug: post.id }, + props: { post }, + })); +}) satisfies GetStaticPaths; + +type Props = InferGetStaticPropsType; + +const { post } = Astro.props; +const { Content } = await render(post); +--- + + + +