Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,3 @@
![Screenshot of nickoates.com](https://repository-images.githubusercontent.com/622056687/77b6322b-5d3d-4bc2-bf35-18f2ba1c809a)

This is my website, blog, and portfolio. I designed and built it myself using Next.js, Tailwind CSS, TypeScript, and MDX. For more information on the tech stack, see [my initial blog post](https://nickoates.com/blog/rebuilding-my-website). Parts of the design were inspired by [Lee Robinson's website](https://leerob.io) and [Apple's newsroom](https://www.apple.com/newsroom).

## Setting up newsletter subscriptions

I use [Resend](https://resend.com) to manage my newsletter. To setup your own newsletter, create a Resend account (if you haven't already), then add an audience for your newsletter. Generate an API key with full access to your account, and add both the key and audience ID to your `.env.local` file:

```bash
RESEND_API_KEY=re_yourApiKey
RESEND_AUDIENCE_ID=your-audience-id
```
123 changes: 84 additions & 39 deletions app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,93 @@ import { Prose } from "@/components/Prose";
import mergeMetadata from "@/lib/mergeMetadata";
import Image from "next/image";
import me from "../../public/images/nick-oates.webp";
import { Orb } from "@/components/Orb";
import { OrbContainer } from "@/components/Orb";
import { S, CustomLink } from "@/components/typography";
import { ViewTransition } from "react";
import { CustomLink } from "@/components/typography";

export const metadata = mergeMetadata({
title: "About",
title: "About",
});

export default function About() {
return (
<Prose>
<OrbContainer>
<Orb className="right-0 bg-blue-400/30 dark:bg-blue-600/30" />
<Orb className="top-28 right-36 bg-purple-400/30 dark:bg-purple-600/30" />
</OrbContainer>

<h1 className="mb-4 text-3xl font-extrabold md:text-4xl">About<span className="sr-only"> Nick Oates</span></h1>

<figure className="float-right ml-4 mb-4! mt-0!">
<Image src={me} width={176} alt="Nick Oates standing in front of of a tree" placeholder="blur" className="outline -outline-offset-1 outline-white/10 w-36 sm:w-44" />
<figcaption className="text-xs!">Photo of me by <CustomLink href="https://ajdimke.com" className="not-prose underline hover:text-orange-500">AJ Dimke</CustomLink></figcaption>
</figure>

<ViewTransition name="about-lead">
<p>
Hey, I&apos;m <S>Nick Oates</S>, a software engineer with a passion for designing and building cool things on the web. I love obsessing over the small details of my work, and I&apos;m always looking for new things to learn and ways to improve my skills.
</p>
</ViewTransition>

<p>
I&apos;m a strong believer in internet freedom & privacy, and I believe those values are essential to a healthy, open web. I&apos;m also a big fan of open-source software, reflected in much of <CustomLink href="/projects">my work</CustomLink>.
</p>

<p>
Outside of tech, I&apos;m an editor of my high school&apos;s newspaper, <CustomLink href="https://bensonorbit.com/staff_name/nick-oates/">The Benson Orbit</CustomLink>, which I helped start in 2024. I cover school news and culture, and handle our digital presence. I&apos;m also the Chief of Music at <CustomLink href="https://kbps.am">KBPS</CustomLink>, the oldest high school radio station in America, where I help manage the music library, DJ on-air, and write and announce news. I have a lot of fun playing video games, especially with friends, with my favorites being <CustomLink href="https://en.wikipedia.org/wiki/Portal_2">Portal 2</CustomLink>, <CustomLink href="https://en.wikipedia.org/wiki/Minecraft">Minecraft</CustomLink>, and <CustomLink href="https://en.wikipedia.org/wiki/The_Legend_of_Zelda:_Skyward_Sword">Zelda: Skyward Sword</CustomLink>, in that order.
</p>

<p>
You can follow me on <CustomLink href="https://x.com/nickoates_">Twitter</CustomLink> or check out my code on <CustomLink href="https://github.com/n1ckoates">GitHub</CustomLink> to see what I&apos;m working on, or if you&apos;d like to get in touch, send me an email at <CustomLink href="mailto:hey@nickoates.com">hey@nickoates.com</CustomLink>.
</p>

</Prose>
)
return (
<Prose>
<h1>
About<span className="sr-only"> Nick Oates</span>
</h1>

<figure className="float-right mt-0! mb-4! ml-5">
<Image
src={me}
width={144}
alt="Nick Oates standing in front of a tree"
placeholder="blur"
className="w-32 rounded-lg outline -outline-offset-1 outline-white/10 sm:w-36"
/>
<figcaption className="text-xs!">
Photo by{" "}
<CustomLink
href="https://ajdimke.com"
className="not-prose underline hover:text-orange-500"
>
AJ Dimke
</CustomLink>
</figcaption>
</figure>

<p className="lead">
I&apos;m a software engineer at Vercel, where I work on the{" "}
<CustomLink href="https://ai-sdk.dev">AI SDK</CustomLink>. I care about
thoughtful developer tools, open-source software, and the small details
that make technology feel good to use.
</p>

<p>
I&apos;m a strong believer in internet freedom and privacy, and I
believe those values are essential to a healthy, open web. They&apos;re
reflected in much of <CustomLink href="/projects">my work</CustomLink>.
</p>

<p>
I <CustomLink href="/blog/graduating-high-school">graduated</CustomLink>{" "}
from Benson Polytechnic High School in 2026, where I majored in radio
broadcasting and gave a valedictorian speech. While I was there, I
helped start the school newspaper,{" "}
<CustomLink href="https://bensonorbit.com/staff_name/nick-oates/">
The Benson Orbit
</CustomLink>
, and served as Chief of Music at{" "}
<CustomLink href="https://kbps.am">KBPS</CustomLink>, the oldest high
school radio station in America.
</p>

<p>
Outside of work, I like writing on{" "}
<CustomLink href="/blog">my blog</CustomLink> and playing video games
with friends. My favorites are{" "}
<CustomLink href="https://en.wikipedia.org/wiki/Portal_2">
Portal 2
</CustomLink>
,{" "}
<CustomLink href="https://en.wikipedia.org/wiki/Minecraft">
Minecraft
</CustomLink>
, and{" "}
<CustomLink href="https://en.wikipedia.org/wiki/The_Legend_of_Zelda:_Skyward_Sword">
Zelda: Skyward Sword
</CustomLink>
, in that order.
</p>

<p>
You can follow me on{" "}
<CustomLink href="https://x.com/nickoates_">Twitter</CustomLink>, see
what I&apos;m building on{" "}
<CustomLink href="https://github.com/n1ckoates">GitHub</CustomLink>, or
email me at{" "}
<CustomLink href="mailto:hey@nickoates.com">
hey@nickoates.com
</CustomLink>
. Thanks for stopping by my website!
</p>
</Prose>
);
}
29 changes: 8 additions & 21 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { CustomMDX } from "@/components/typography";
import allPosts from "@/lib/posts";
import "./code.css";
import { Prose } from "@/components/Prose";
import { ViewTransition } from "react";

export const dynamicParams = false; // Blog posts are static, don't attempt to generate dynamic routes

Expand All @@ -17,26 +16,14 @@ export default async function Post(props: Props) {

return (
<Prose>
<ViewTransition name={`${post._meta.path}-grid-time`}>
<ViewTransition name={`${post._meta.path}-search-time`}>
<span>
<time dateTime={post.date.toISOString()}>
{post.date.toLocaleDateString(undefined, { dateStyle: "long" })}
</time>{" "}
&bull; {post.readingTime} min read
</span>
</ViewTransition>
</ViewTransition>


<ViewTransition name={`${post._meta.path}-grid-title`}>
<ViewTransition name={`${post._meta.path}-search-title`}>
<h1
>
{post.title}
</h1>
</ViewTransition>
</ViewTransition>
<span>
<time dateTime={post.date.toISOString()}>
{post.date.toLocaleDateString(undefined, { dateStyle: "long" })}
</time>{" "}
&bull; {post.readingTime} min read
</span>

<h1>{post.title}</h1>

<CustomMDX code={post.mdx} />
</Prose>
Expand Down
99 changes: 51 additions & 48 deletions app/blog/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import Image from "next/image";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import TextInput from "@/components/TextInput";
import { useFuzzySearchList } from "@nozbe/microfuzz/react";
import { ViewTransition } from "react";

type PartialBlogPost = {
slug: string;
Expand All @@ -20,9 +19,7 @@ type PartialBlogPost = {
blurDataURL: string;
};

export function BlogClient(props: {
posts: PartialBlogPost[];
}) {
export function BlogClient(props: { posts: PartialBlogPost[] }) {
const [queryText, setQueryText] = useState("");
const [animationParent] = useAutoAnimate();

Expand All @@ -35,70 +32,76 @@ export function BlogClient(props: {

return (
<>
<div className="mb-4 flex flex-col justify-between gap-2 lg:flex-row">
<ViewTransition name="blog-posts-header">
<h1 className="text-3xl font-extrabold md:text-4xl">Blog Posts</h1>
</ViewTransition>
<div className="mb-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-end">
<div>
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
Blog
</h1>
<p className="text-muted-foreground mt-1 text-sm">
Notes on software, the web, and things I&apos;m learning.
</p>
</div>

<div className="relative w-full lg:w-2/3">
<div className="relative w-full sm:w-64">
<TextInput
name="search"
className="w-full rounded-md"
className="w-full rounded-lg pr-10 text-sm"
placeholder="Search posts..."
aria-label="Search posts"
onChange={(e) => setQueryText(e.target.value)}
/>
<IconSearch className="absolute top-2 right-4" />
<IconSearch
className="text-muted-foreground absolute top-2.5 right-3"
size={18}
aria-hidden
/>
</div>
</div>

<div ref={animationParent}>
<div ref={animationParent} className="space-y-3">
{!filteredPosts.length && (
<p>No posts were found with that search term.</p>
<p className="border-border text-muted-foreground rounded-xl border p-5 text-sm">
No posts were found with that search term.
</p>
)}
{filteredPosts.map(({ post }) => (
<Link
href={"/blog/" + post.slug}
key={post.slug}
className="group relative mb-4 block overflow-hidden rounded-lg border border-zinc-200 drop-shadow-sm dark:border-zinc-800 dark:bg-neutral-900/30"
className="border-border bg-surface hover:bg-surface-hover grid overflow-hidden rounded-xl border sm:grid-cols-[10rem_minmax(0,1fr)]"
aria-label={post.title}
>
<Image
alt={post.coverAlt}
src={post.cover}
fill
className="absolute -z-10 object-cover brightness-50 transition group-hover:scale-105"
placeholder="blur"
blurDataURL={post.blurDataURL}
/>
<div className="bg-secondary relative aspect-video overflow-hidden sm:aspect-auto">
<Image
alt={post.coverAlt}
src={post.cover}
fill
className="object-cover"
sizes="(max-width:640px) 100vw, 160px"
placeholder="blur"
blurDataURL={post.blurDataURL}
/>
</div>

<div className="min-w-0 p-4 sm:p-5">
<p className="text-muted-foreground mb-1.5 text-xs font-medium">
<time dateTime={post.date.toISOString()}>
{post.date.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
})}
</time>{" "}
&bull; {post.readingTime} min read
</p>

<div className="flex flex-col justify-between gap-1 bg-linear-to-b from-transparent to-zinc-950 p-4 text-white lg:flex-row">
<ViewTransition name={`${post.slug}-search-time`}>
<div className="text-zinc-300 md:text-lg">
<time
dateTime={post.date.toISOString()}
className="whitespace-pre after:content-['_•_'] lg:after:content-['\A']"
>
{post.date.toLocaleDateString(undefined, {
dateStyle: "long",
})}
</time>
{post.readingTime} min read
</div>
</ViewTransition>
<hgroup className="w-full lg:w-2/3">
<ViewTransition name={`${post.slug}-search-title`}>
<h2
className="mb-1 text-xl font-bold md:text-2xl"
>
{post.title}
</h2>
</ViewTransition>
<h2 className="text-lg leading-snug font-semibold tracking-tight sm:text-xl">
{post.title}
</h2>

<p className="text-lg text-zinc-300 md:text-xl">
{post.summary}
</p>
</hgroup>
<p className="text-subtle-foreground mt-1.5 line-clamp-2 text-sm leading-6">
{post.summary}
</p>
</div>
</Link>
))}
Expand Down
4 changes: 2 additions & 2 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { BlogClient } from "./page-client"
import { BlogClient } from "./page-client";
import allPosts from "@/lib/posts";
import mergeMetadata from "@/lib/mergeMetadata";

export const metadata = mergeMetadata({ title: "Blog Posts" });
export const metadata = mergeMetadata({ title: "Blog" });

export default function Blog() {
// Remove unneeded fields from posts before sending them to the client
Expand Down
Loading