initial commit

This commit is contained in:
Cory Dransfeldt 2022-05-21 17:27:30 -07:00
commit d799808203
126 changed files with 16265 additions and 0 deletions

24
pages/404.tsx Normal file
View file

@ -0,0 +1,24 @@
import Link from '@/components/Link'
export default function FourZeroFour() {
return (
<div className="flex flex-col items-start justify-start md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6">
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:border-r-2 md:px-6 md:text-8xl md:leading-14">
404
</h1>
</div>
<div className="max-w-md">
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
Sorry we couldn't find this page.
</p>
<p className="mb-8">But dont worry, you can find plenty of other things on our homepage.</p>
<Link href="/">
<button className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
Back to homepage
</button>
</Link>
</div>
</div>
)
}

32
pages/_app.tsx Normal file
View file

@ -0,0 +1,32 @@
import '@/css/tailwind.css'
import '@/css/prism.css'
import 'katex/dist/katex.css'
import '@fontsource/inter/variable-full.css'
import { ThemeProvider } from 'next-themes'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import siteMetadata from '@/data/siteMetadata'
import Analytics from '@/components/analytics'
import LayoutWrapper from '@/components/LayoutWrapper'
import { ClientReload } from '@/components/ClientReload'
const isDevelopment = process.env.NODE_ENV === 'development'
const isSocket = process.env.SOCKET
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
<Head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
</Head>
{isDevelopment && isSocket && <ClientReload />}
<Analytics />
<LayoutWrapper>
<Component {...pageProps} />
</LayoutWrapper>
</ThemeProvider>
)
}

36
pages/_document.tsx Normal file
View file

@ -0,0 +1,36 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
<Html lang="en" className="scroll-smooth">
<Head>
<link rel="apple-touch-icon" sizes="76x76" href="/static/favicons/apple-touch-icon.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/static/favicons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/static/favicons/favicon-16x16.png"
/>
<link rel="manifest" href="/static/favicons/site.webmanifest" />
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#000000" />
<meta name="theme-color" content="#000000" />
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
</Head>
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

27
pages/about.tsx Normal file
View file

@ -0,0 +1,27 @@
import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { getFileBySlug } from '@/lib/mdx'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
const DEFAULT_LAYOUT = 'AuthorLayout'
// @ts-ignore
export const getStaticProps: GetStaticProps<{
authorDetails: { mdxSource: string; frontMatter: AuthorFrontMatter }
}> = async () => {
const authorDetails = await getFileBySlug<AuthorFrontMatter>('authors', ['default'])
const { mdxSource, frontMatter } = authorDetails
return { props: { authorDetails: { mdxSource, frontMatter } } }
}
export default function About({ authorDetails }: InferGetStaticPropsType<typeof getStaticProps>) {
const { mdxSource, frontMatter } = authorDetails
return (
<MDXLayoutRenderer
layout={frontMatter.layout || DEFAULT_LAYOUT}
mdxSource={mdxSource}
frontMatter={frontMatter}
/>
)
}

32
pages/api/buttondown.ts Normal file
View file

@ -0,0 +1,32 @@
import { NextApiRequest, NextApiResponse } from 'next'
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })
}
try {
const API_KEY = process.env.BUTTONDOWN_API_KEY
const buttondownRoute = `${process.env.BUTTONDOWN_API_URL}subscribers`
const response = await fetch(buttondownRoute, {
body: JSON.stringify({
email,
}),
headers: {
Authorization: `Token ${API_KEY}`,
'Content-Type': 'application/json',
},
method: 'POST',
})
if (response.status >= 400) {
return res.status(500).json({ error: `There was an error subscribing to the list.` })
}
return res.status(201).json({ error: '' })
} catch (error) {
return res.status(500).json({ error: error.message || error.toString() })
}
}

37
pages/api/convertkit.ts Normal file
View file

@ -0,0 +1,37 @@
import { NextApiRequest, NextApiResponse } from 'next'
/* eslint-disable import/no-anonymous-default-export */
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })
}
try {
const FORM_ID = process.env.CONVERTKIT_FORM_ID
const API_KEY = process.env.CONVERTKIT_API_KEY
const API_URL = process.env.CONVERTKIT_API_URL
// Send request to ConvertKit
const data = { email, api_key: API_KEY }
const response = await fetch(`${API_URL}forms/${FORM_ID}/subscribe`, {
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
if (response.status >= 400) {
return res.status(400).json({
error: `There was an error subscribing to the list.`,
})
}
return res.status(201).json({ error: '' })
} catch (error) {
return res.status(500).json({ error: error.message || error.toString() })
}
}

37
pages/api/klaviyo.ts Normal file
View file

@ -0,0 +1,37 @@
import { NextApiRequest, NextApiResponse } from 'next'
/* eslint-disable import/no-anonymous-default-export */
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })
}
try {
const API_KEY = process.env.KLAVIYO_API_KEY
const LIST_ID = process.env.KLAVIYO_LIST_ID
const response = await fetch(
`https://a.klaviyo.com/api/v2/list/${LIST_ID}/subscribe?api_key=${API_KEY}`,
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
// You can add additional params here i.e. SMS, etc.
// https://developers.klaviyo.com/en/reference/subscribe
body: JSON.stringify({
profiles: [{ email: email }],
}),
}
)
if (response.status >= 400) {
return res.status(400).json({
error: `There was an error subscribing to the list.`,
})
}
return res.status(201).json({ error: '' })
} catch (error) {
return res.status(500).json({ error: error.message || error.toString() })
}
}

26
pages/api/mailchimp.ts Normal file
View file

@ -0,0 +1,26 @@
import { NextApiRequest, NextApiResponse } from 'next'
import mailchimp from '@mailchimp/mailchimp_marketing'
mailchimp.setConfig({
apiKey: process.env.MAILCHIMP_API_KEY,
server: process.env.MAILCHIMP_API_SERVER, // E.g. us1
})
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextApiRequest, res: NextApiResponse) => {
const { email } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })
}
try {
await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
email_address: email,
status: 'subscribed',
})
return res.status(201).json({ error: '' })
} catch (error) {
return res.status(500).json({ error: error.message || error.toString() })
}
}

41
pages/blog.tsx Normal file
View file

@ -0,0 +1,41 @@
import { getAllFilesFrontMatter } from '@/lib/mdx'
import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'
import { PageSEO } from '@/components/SEO'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { ComponentProps } from 'react'
export const POSTS_PER_PAGE = 5
export const getStaticProps: GetStaticProps<{
posts: ComponentProps<typeof ListLayout>['posts']
initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts']
pagination: ComponentProps<typeof ListLayout>['pagination']
}> = async () => {
const posts = await getAllFilesFrontMatter('blog')
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
const pagination = {
currentPage: 1,
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
}
return { props: { initialDisplayPosts, posts, pagination } }
}
export default function Blog({
posts,
initialDisplayPosts,
pagination,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
<ListLayout
posts={posts}
initialDisplayPosts={initialDisplayPosts}
pagination={pagination}
title="All Posts"
/>
</>
)
}

94
pages/blog/[...slug].tsx Normal file
View file

@ -0,0 +1,94 @@
import fs from 'fs'
import PageTitle from '@/components/PageTitle'
import generateRss from '@/lib/generate-rss'
import { MDXLayoutRenderer } from '@/components/MDXComponents'
import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
import { PostFrontMatter } from 'types/PostFrontMatter'
import { Toc } from 'types/Toc'
const DEFAULT_LAYOUT = 'PostLayout'
export async function getStaticPaths() {
const posts = getFiles('blog')
return {
paths: posts.map((p) => ({
params: {
slug: formatSlug(p).split('/'),
},
})),
fallback: false,
}
}
// @ts-ignore
export const getStaticProps: GetStaticProps<{
post: { mdxSource: string; toc: Toc; frontMatter: PostFrontMatter }
authorDetails: AuthorFrontMatter[]
prev?: { slug: string; title: string }
next?: { slug: string; title: string }
}> = async ({ params }) => {
const slug = (params.slug as string[]).join('/')
const allPosts = await getAllFilesFrontMatter('blog')
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === slug)
const prev: { slug: string; title: string } = allPosts[postIndex + 1] || null
const next: { slug: string; title: string } = allPosts[postIndex - 1] || null
const post = await getFileBySlug<PostFrontMatter>('blog', slug)
// @ts-ignore
const authorList = post.frontMatter.authors || ['default']
const authorPromise = authorList.map(async (author) => {
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [author])
return authorResults.frontMatter
})
const authorDetails = await Promise.all(authorPromise)
// rss
if (allPosts.length > 0) {
const rss = generateRss(allPosts)
fs.writeFileSync('./public/feed.xml', rss)
}
return {
props: {
post,
authorDetails,
prev,
next,
},
}
}
export default function Blog({
post,
authorDetails,
prev,
next,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const { mdxSource, toc, frontMatter } = post
return (
<>
{'draft' in frontMatter && frontMatter.draft !== true ? (
<MDXLayoutRenderer
layout={frontMatter.layout || DEFAULT_LAYOUT}
toc={toc}
mdxSource={mdxSource}
frontMatter={frontMatter}
authorDetails={authorDetails}
prev={prev}
next={next}
/>
) : (
<div className="mt-24 text-center">
<PageTitle>
Under Construction{' '}
<span role="img" aria-label="roadwork sign">
🚧
</span>
</PageTitle>
</div>
)}
</>
)
}

View file

@ -0,0 +1,66 @@
import { PageSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import ListLayout from '@/layouts/ListLayout'
import { POSTS_PER_PAGE } from '../../blog'
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
import { PostFrontMatter } from 'types/PostFrontMatter'
export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
const totalPosts = await getAllFilesFrontMatter('blog')
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
const paths = Array.from({ length: totalPages }, (_, i) => ({
params: { page: (i + 1).toString() },
}))
return {
paths,
fallback: false,
}
}
export const getStaticProps: GetStaticProps<{
posts: PostFrontMatter[]
initialDisplayPosts: PostFrontMatter[]
pagination: { currentPage: number; totalPages: number }
}> = async (context) => {
const {
params: { page },
} = context
const posts = await getAllFilesFrontMatter('blog')
const pageNumber = parseInt(page as string)
const initialDisplayPosts = posts.slice(
POSTS_PER_PAGE * (pageNumber - 1),
POSTS_PER_PAGE * pageNumber
)
const pagination = {
currentPage: pageNumber,
totalPages: Math.ceil(posts.length / POSTS_PER_PAGE),
}
return {
props: {
posts,
initialDisplayPosts,
pagination,
},
}
}
export default function PostPage({
posts,
initialDisplayPosts,
pagination,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
<ListLayout
posts={posts}
initialDisplayPosts={initialDisplayPosts}
pagination={pagination}
title="All Posts"
/>
</>
)
}

102
pages/index.tsx Normal file
View file

@ -0,0 +1,102 @@
import Link from '@/components/Link'
import { PageSEO } from '@/components/SEO'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import formatDate from '@/lib/utils/formatDate'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { PostFrontMatter } from 'types/PostFrontMatter'
import NewsletterForm from '@/components/NewsletterForm'
const MAX_DISPLAY = 5
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[] }> = async () => {
const posts = await getAllFilesFrontMatter('blog')
return { props: { posts } }
}
export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
Latest
</h1>
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
{siteMetadata.description}
</p>
</div>
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
{!posts.length && 'No posts found.'}
{posts.slice(0, MAX_DISPLAY).map((frontMatter) => {
const { slug, date, title, summary, tags } = frontMatter
return (
<li key={slug} className="py-12">
<article>
<div className="space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0">
<dl>
<dt className="sr-only">Published on</dt>
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
<time dateTime={date}>{formatDate(date)}</time>
</dd>
</dl>
<div className="space-y-5 xl:col-span-3">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold leading-8 tracking-tight">
<Link
href={`/blog/${slug}`}
className="text-gray-900 dark:text-gray-100"
>
{title}
</Link>
</h2>
<div className="flex flex-wrap">
{tags.map((tag) => (
<Tag key={tag} text={tag} />
))}
</div>
</div>
<div className="prose max-w-none text-gray-500 dark:text-gray-400">
{summary}
</div>
</div>
<div className="text-base font-medium leading-6">
<Link
href={`/blog/${slug}`}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
aria-label={`Read "${title}"`}
>
Read more &rarr;
</Link>
</div>
</div>
</div>
</article>
</li>
)
})}
</ul>
</div>
{posts.length > MAX_DISPLAY && (
<div className="flex justify-end text-base font-medium leading-6">
<Link
href="/blog"
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
aria-label="all posts"
>
All Posts &rarr;
</Link>
</div>
)}
{siteMetadata.newsletter.provider !== '' && (
<div className="flex items-center justify-center pt-4">
<NewsletterForm />
</div>
)}
</>
)
}

35
pages/projects.tsx Normal file
View file

@ -0,0 +1,35 @@
import siteMetadata from '@/data/siteMetadata'
import projectsData from '@/data/projectsData'
import Card from '@/components/Card'
import { PageSEO } from '@/components/SEO'
export default function Projects() {
return (
<>
<PageSEO title={`Projects - ${siteMetadata.author}`} description={siteMetadata.description} />
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
Projects
</h1>
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
Showcase your projects with a hero image (16 x 9)
</p>
</div>
<div className="container py-12">
<div className="-m-4 flex flex-wrap">
{projectsData.map((d) => (
<Card
key={d.title}
title={d.title}
description={d.description}
imgSrc={d.imgSrc}
href={d.href}
/>
))}
</div>
</div>
</div>
</>
)
}

45
pages/tags.tsx Normal file
View file

@ -0,0 +1,45 @@
import Link from '@/components/Link'
import { PageSEO } from '@/components/SEO'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
export const getStaticProps: GetStaticProps<{ tags: Record<string, number> }> = async () => {
const tags = await getAllTags('blog')
return { props: { tags } }
}
export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticProps>) {
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
return (
<>
<PageSEO title={`Tags - ${siteMetadata.author}`} description="Things I blog about" />
<div className="flex flex-col items-start justify-start divide-y divide-gray-200 dark:divide-gray-700 md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6 md:divide-y-0">
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:border-r-2 md:px-6 md:text-6xl md:leading-14">
Tags
</h1>
</div>
<div className="flex max-w-lg flex-wrap">
{Object.keys(tags).length === 0 && 'No tags found.'}
{sortedTags.map((t) => {
return (
<div key={t} className="mt-2 mb-2 mr-5">
<Tag text={t} />
<Link
href={`/tags/${kebabCase(t)}`}
className="-ml-2 text-sm font-semibold uppercase text-gray-600 dark:text-gray-300"
>
{` (${tags[t]})`}
</Link>
</div>
)
})}
</div>
</div>
</>
)
}

60
pages/tags/[tag].tsx Normal file
View file

@ -0,0 +1,60 @@
import { TagSEO } from '@/components/SEO'
import siteMetadata from '@/data/siteMetadata'
import ListLayout from '@/layouts/ListLayout'
import generateRss from '@/lib/generate-rss'
import { getAllFilesFrontMatter } from '@/lib/mdx'
import { getAllTags } from '@/lib/tags'
import kebabCase from '@/lib/utils/kebabCase'
import fs from 'fs'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import path from 'path'
import { PostFrontMatter } from 'types/PostFrontMatter'
const root = process.cwd()
export async function getStaticPaths() {
const tags = await getAllTags('blog')
return {
paths: Object.keys(tags).map((tag) => ({
params: {
tag,
},
})),
fallback: false,
}
}
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[]; tag: string }> = async (
context
) => {
const tag = context.params.tag as string
const allPosts = await getAllFilesFrontMatter('blog')
const filteredPosts = allPosts.filter(
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(tag)
)
// rss
if (filteredPosts.length > 0) {
const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`)
const rssPath = path.join(root, 'public', 'tags', tag)
fs.mkdirSync(rssPath, { recursive: true })
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
}
return { props: { posts: filteredPosts, tag } }
}
export default function Tag({ posts, tag }: InferGetStaticPropsType<typeof getStaticProps>) {
// Capitalize first letter and convert space to dash
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
return (
<>
<TagSEO
title={`${tag} - ${siteMetadata.title}`}
description={`${tag} tags - ${siteMetadata.author}`}
/>
<ListLayout posts={posts} title={title} />
</>
)
}