Update website to use typescript

This commit is contained in:
Jip J. Dekker 2023-11-29 21:20:24 +11:00
parent 21a0a1f2d7
commit 8f9757936e
No known key found for this signature in database
55 changed files with 2203 additions and 1417 deletions

View File

@ -3,7 +3,8 @@ packages:
- npm
oauth: pages.sr.ht/PAGES:RW
environment:
site: dekker.one
site: "dekker.one"
NEXT_PUBLIC_SITE_URL: "https://dekker.one"
tasks:
- package: |
cd $site

1
.env.example Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_SITE_URL=https://example.com

6
.gitignore vendored
View File

@ -23,7 +23,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
@ -31,5 +30,6 @@ yarn-error.log*
# vercel
.vercel
# generated files
/public/rss/
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -10,6 +10,12 @@ To get started with this template, first install the npm dependencies:
npm install
```
Next, create a `.env.local` file in the root of your project and set the `NEXT_PUBLIC_SITE_URL` variable to your site's public URL:
```
NEXT_PUBLIC_SITE_URL=https://example.com
```
Next, run the development server:
```bash

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

7
mdx-components.tsx Normal file
View File

@ -0,0 +1,7 @@
import { type MDXComponents } from 'mdx/types'
export function useMDXComponents(components: MDXComponents) {
return {
...components,
}
}

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,14 +1,10 @@
import rehypePrism from '@mapbox/rehype-prism'
import nextMDX from '@next/mdx'
import remarkGfm from 'remark-gfm'
import rehypePrism from '@mapbox/rehype-prism'
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['js', 'jsx', 'mdx'],
reactStrictMode: true,
experimental: {
scrollRestoration: true,
},
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
output: 'export',
images: {
unoptimized: true,

763
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,24 +17,28 @@
"@mdx-js/react": "^3.0.0",
"@next/mdx": "^14.0.3",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.10.0",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.17",
"@types/webpack-env": "^1.18.4",
"autoprefixer": "^10.4.16",
"cheerio": "^1.0.0-rc.12",
"clsx": "^2.0.0",
"fast-glob": "^3.3.2",
"feed": "^4.2.2",
"focus-visible": "^5.2.0",
"next": "^14.0.3",
"next-router-mock": "^0.9.10",
"postcss-focus-visible": "^9.0.0",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"remark-gfm": "^4.0.0",
"tailwindcss": "^3.3.5"
"tailwindcss": "^3.3.5",
"typescript": "^5.3.2"
},
"devDependencies": {
"@catppuccin/tailwindcss": "^0.1.6",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.3",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7"
"prettier-plugin-tailwindcss": "^0.5.7",
"sharp": "^0.32.6"
}
}

View File

@ -1,9 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
'postcss-focus-visible': {
replaceWith: '[data-focus-visible-added]',
},
autoprefixer: {},
},
}

View File

@ -1,5 +1,6 @@
/** @type {import('prettier').Options} */
module.exports = {
singleQuote: true,
semi: false,
plugins: [import('prettier-plugin-tailwindcss')],
plugins: ['prettier-plugin-tailwindcss'],
}

161
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,161 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { Container } from '@/components/Container'
import {
GitHubIcon,
LinkedInIcon,
MastodonIcon,
ORCIDIcon,
ResearchGateIcon,
} from '@/components/SVGIcons'
import portraitImage from '@/images/portrait.jpg'
function SocialLink({
className,
href,
children,
icon: Icon,
}: {
className?: string
href: string
icon: React.ComponentType<{ className?: string }>
children: React.ReactNode
}) {
return (
<li className={clsx(className, 'flex')}>
<Link
href={href}
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
>
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
<span className="ml-4">{children}</span>
</Link>
</li>
)
}
function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
d="M6 5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6Zm.245 2.187a.75.75 0 0 0-.99 1.126l6.25 5.5a.75.75 0 0 0 .99 0l6.25-5.5a.75.75 0 0 0-.99-1.126L12 12.251 6.245 7.187Z"
/>
</svg>
)
}
export const metadata: Metadata = {
title: 'About',
description:
'Im dr. Jip J. Dekker, a problem solver using optimization technologies.',
}
export default function About() {
return (
<Container className="mt-16 sm:mt-32">
<div className="grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
<div className="lg:pl-20">
<div className="max-w-xs px-2.5 lg:max-w-none">
<Image
src={portraitImage}
alt=""
sizes="(min-width: 1024px) 32rem, 20rem"
className="aspect-square rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
/>
</div>
</div>
<div className="lg:order-first lg:row-span-2">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
Im dr. Jip J. Dekker, a problem solver using optimization
technologies.
</h1>
<div className="mt-6 space-y-7 text-base text-zinc-600 dark:text-zinc-400">
<p>
Im a computer scientist at Monash University specializing in
optimization and devoted to developing cutting-edge tools and
modelling languages that simplify the process of solving complex
problems. With a passion for advancing the field, I contribute to
the optimization field, enhancing its efficiency, and facilitating
its practical application.
</p>
<p>
Building from a foundation in constraint programming, I have
gained experience in Boolean satisfiability, mathematical
modelling, local search, various hybrid method. With the wide
range of optimization technologies at my fingertips, it enables me
to approach optimization challenges from many angles. This allows
me to facilitate the creation of innovative software solutions
that streamline the formulation, solution, and analysis of
optimization problems.
</p>
<p>
At the heart of my work is a commitment to making optimization
more accessible and user-friendly. By developing intuitive tools
and modelling languages, I want to empower researchers,
practitioners, and anyone willing to learn to effectively address
complex optimization challenges across a wide range of domains. I
hope that my work not only enhances the effectiveness of
optimization technologies, but also fosters innovation and
improvements in other industries.
</p>
<p>
When Im not solving optimization problems, I love bird watching,
music, and cycling. The beauty of the avian world has captivated
me ever since I moved to Australia. Music brings me solace. Im
always looking for new songs with interesting melodies or striking
lyrics. Cycling always gives me a sense of freedom. You find the
most wonderful places, if you stray from the beaten track even
slightly. These pursuits bring me joy and inspiration.
</p>
</div>
</div>
<div className="lg:pl-20">
<ul role="list">
<SocialLink href="https://github.com/Dekker1" icon={GitHubIcon}>
Follow me on GitHub
</SocialLink>
<SocialLink
href="https://soapbox.network/@Dekker1"
icon={MastodonIcon}
className="mt-4"
>
Follow me on Mastodon
</SocialLink>
<SocialLink
href="https://www.linkedin.com/in/dekker1/"
icon={LinkedInIcon}
className="mt-4"
>
Connect with me on LinkedIn
</SocialLink>
<SocialLink
href="https://orcid.org/0000-0002-0053-6724"
icon={ORCIDIcon}
className="mt-4"
>
Find me on ORCID
</SocialLink>
<SocialLink
href="https://www.researchgate.net/profile/Jip-Dekker-2"
icon={ResearchGateIcon}
className="mt-4"
>
Follow me on ResearchGate
</SocialLink>
<SocialLink
href="mailto:jip.dekker@monash.edu"
icon={MailIcon}
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
>
jip.dekker@monash.edu
</SocialLink>
</ul>
</div>
</div>
</Container>
)
}

View File

@ -1,14 +1,19 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const meta = {
export const dynamic = 'force-static'
export const article = {
author: 'Jip J. Dekker',
date: '2017-02-24',
title: 'Implementing custom DTrace instrumentation',
description:
'DTrace (and SystemTap) are often the “go to” when adding tracing in high performance environments such as for example operating systems. This note discusses adding instrumentation to your own application, so you can take advantage of these powerful tools.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout meta={meta} {...props} />
export default (props) => <ArticleLayout article={article} {...props} />
Last semester I had a chance to work with DTrace. In particular, I implemented
custom DTrace instrumentation in Encore and [Pony](http://www.ponylang.org/).

View File

@ -1,14 +1,19 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const meta = {
export const dynamic = 'force-static'
export const article = {
author: 'Jip J. Dekker',
date: '2022-08-15',
title: 'A Homebrew Tap for MiniZinc Solvers',
description:
"I'm proposing a Homebrew tap to make it easier for users to install different MiniZinc solvers. The tap already contains many of the open source solvers that are current contenders in the MiniZinc challenge, and I'm hoping to add any others that fit the infrastructure.",
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout meta={meta} {...props} />
export default (props) => <ArticleLayout article={article} {...props} />
TLDR; I'm proposing a [Homebrew](https://brew.sh/) tap to make it easier for users to install different MiniZinc solvers.
The tap already contains many of the open source solvers that are current contenders in the MiniZinc challenge, and I'm hoping to add any others that fit the infrastructure.

60
src/app/articles/page.tsx Normal file
View File

@ -0,0 +1,60 @@
import { type Metadata } from 'next'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import { type ArticleWithSlug, getAllArticles } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
function Article({ article }: { article: ArticleWithSlug }) {
return (
<article className="md:grid md:grid-cols-4 md:items-baseline">
<Card className="md:col-span-3">
<Card.Title href={`/articles/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="md:hidden"
decorate
>
{formatDate(article.date)}
</Card.Eyebrow>
<Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta>
</Card>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="mt-1 hidden md:block"
>
{formatDate(article.date)}
</Card.Eyebrow>
</article>
)
}
export const metadata: Metadata = {
title: 'Articles',
description:
'All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order.',
}
export default async function ArticlesIndex() {
let articles = await getAllArticles()
return (
<SimpleLayout
title="Writing on optimization, programming languages."
intro="All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order."
>
<div className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
<div className="flex max-w-3xl flex-col space-y-16">
{articles.map((article) => (
<Article key={article.slug} article={article} />
))}
</div>
</div>
</SimpleLayout>
)
}

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

62
src/app/feed.xml/route.ts Normal file
View File

@ -0,0 +1,62 @@
import { getAllArticles } from '@/lib/articles'
import assert from 'assert'
import { Feed } from 'feed'
export async function GET(req: Request) {
let siteUrl = process.env.NEXT_PUBLIC_SITE_URL
if (!siteUrl) {
throw Error('Missing NEXT_PUBLIC_SITE_URL environment variable')
}
let author = {
name: 'Jip J. Dekker',
email: 'jip.dekker@monash.edu',
}
let feed = new Feed({
title: author.name,
description:
'The collection of writing by Jip about optimization, programming language, and general computer science',
author,
id: siteUrl,
link: siteUrl,
image: `${siteUrl}/favicon.ico`,
favicon: `${siteUrl}/favicon.ico`,
copyright: `All rights reserved ${new Date().getFullYear()}`,
feedLinks: {
rss2: `${siteUrl}/feed.xml`,
},
})
let articles = await getAllArticles()
for (let article of articles) {
let publicUrl = `${siteUrl}/articles/${article.slug}`
let title = article.title
let date = article.date
let description = article.description
assert(typeof title === 'string')
assert(typeof date === 'string')
assert(typeof description === 'string')
feed.addItem({
title,
id: publicUrl,
link: publicUrl,
description,
author: [author],
contributor: [author],
date: new Date(date),
})
}
return new Response(feed.rss2(), {
status: 200,
headers: {
'content-type': 'application/xml',
'cache-control': 's-maxage=31556952',
},
})
}

38
src/app/layout.tsx Normal file
View File

@ -0,0 +1,38 @@
import { type Metadata } from 'next'
import { Providers } from '@/app/providers'
import { Layout } from '@/components/Layout'
import '@/styles/tailwind.css'
export const metadata: Metadata = {
title: {
template: '%s - Jip J. Dekker',
default: 'Jip J. Dekker - Optimisation Expert & Computer Scientist',
},
description:
'Im Jip, a researcher at the OPTIMA ARC research centre and Monash University, where we aim to make complex decisions easier through decision support and data insights. We design state-of-the-art optimization techniques, such as the MiniZinc modelling language, ready to be used in industry.',
alternates: {
types: {
'application/rss+xml': `${process.env.NEXT_PUBLIC_SITE_URL}/feed.xml`,
},
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="h-full antialiased" suppressHydrationWarning>
<body className="flex h-full bg-zinc-50 dark:bg-black">
<Providers>
<div className="flex w-full">
<Layout>{children}</Layout>
</div>
</Providers>
</body>
</html>
)
}

23
src/app/not-found.tsx Normal file
View File

@ -0,0 +1,23 @@
import { Button } from '@/components/Button'
import { Container } from '@/components/Container'
export default function NotFound() {
return (
<Container className="flex h-full items-center pt-16 sm:pt-32">
<div className="flex flex-col items-center">
<p className="text-base font-semibold text-zinc-400 dark:text-zinc-500">
404
</p>
<h1 className="mt-4 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
Page not found
</h1>
<p className="mt-4 text-base text-zinc-600 dark:text-zinc-400">
Sorry, we couldnt find the page youre looking for.
</p>
<Button href="/" variant="secondary" className="mt-4">
Go back home
</Button>
</div>
</Container>
)
}

View File

@ -1,12 +1,10 @@
import Head from 'next/head'
import Image from 'next/image'
import Image, { type ImageProps } from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { Card } from '@/components/Card'
import { Container } from '@/components/Container'
import {
ArrowDownIcon,
BriefcaseIcon,
GitHubIcon,
LinkedInIcon,
@ -23,12 +21,10 @@ import image2 from '@/images/photos/image-4.jpg'
import image3 from '@/images/photos/image-2.jpg'
import image4 from '@/images/photos/image-3.jpg'
import image5 from '@/images/photos/image-5.jpg'
import { type ArticleWithSlug, getAllArticles } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
import { generateRssFeed } from '@/lib/generateRssFeed'
import { getAllArticles } from '@/lib/getAllArticles'
import { Button } from '@/components/Button'
function Article({ article }) {
function Article({ article }: { article: ArticleWithSlug }) {
return (
<Card as="article">
<Card.Title href={`/articles/${article.slug}`}>
@ -43,7 +39,12 @@ function Article({ article }) {
)
}
function SocialLink({ icon: Icon, ...props }) {
function SocialLink({
icon: Icon,
...props
}: React.ComponentPropsWithoutRef<typeof Link> & {
icon: React.ComponentType<{ className?: string }>
}) {
return (
<Link className="group -m-1 p-1" {...props}>
<Icon className="h-6 w-6 fill-zinc-500 transition group-hover:fill-zinc-600 dark:fill-zinc-400 dark:group-hover:fill-zinc-300" />
@ -51,8 +52,53 @@ function SocialLink({ icon: Icon, ...props }) {
)
}
interface Role {
company: string
title: string
logo: ImageProps['src']
start: string | { label: string; dateTime: string }
end: string | { label: string; dateTime: string }
}
function Role({ role }: { role: Role }) {
let startLabel =
typeof role.start === 'string' ? role.start : role.start.label
let startDate =
typeof role.start === 'string' ? role.start : role.start.dateTime
let endLabel = typeof role.end === 'string' ? role.end : role.end.label
let endDate = typeof role.end === 'string' ? role.end : role.end.dateTime
return (
<li className="flex gap-4">
<div className="relative mt-1 flex h-10 w-10 flex-none items-center justify-center rounded-xl shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<Image src={role.logo} alt="" className="h-7 w-7" unoptimized />
</div>
<dl className="flex flex-auto flex-wrap gap-x-2">
<dt className="sr-only">Company</dt>
<dd className="w-full flex-none text-sm font-medium text-zinc-900 dark:text-zinc-100">
{role.company}
</dd>
<dt className="sr-only">Role</dt>
<dd className="text-xs text-zinc-500 dark:text-zinc-400">
{role.title}
</dd>
<dt className="sr-only">Date</dt>
<dd
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
aria-label={`${startLabel} until ${endLabel}`}
>
<time dateTime={startDate}>{startLabel}</time>{' '}
<span aria-hidden="true"></span>{' '}
<time dateTime={endDate}>{endLabel}</time>
</dd>
</dl>
</li>
)
}
function Resume() {
let resume = [
let resume: Array<Role> = [
{
company: 'OPTIMA & Monash University',
title: 'Research Fellow',
@ -60,7 +106,7 @@ function Resume() {
start: '2021',
end: {
label: 'Present',
dateTime: new Date().getFullYear(),
dateTime: new Date().getFullYear().toString(),
},
},
{
@ -94,53 +140,14 @@ function Resume() {
</h2>
<ol className="mt-6 space-y-4">
{resume.map((role, roleIndex) => (
<li key={roleIndex} className="flex gap-4">
<div className="relative mt-1 flex h-10 w-10 flex-none items-center justify-center rounded-xl shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-200 dark:ring-0">
<Image src={role.logo} alt="" className="h-8 w-8" unoptimized />
</div>
<dl className="flex flex-auto flex-wrap gap-x-2">
<dt className="sr-only">Company</dt>
<dd className="w-full flex-none text-sm font-medium text-zinc-900 dark:text-zinc-100">
{role.company}
</dd>
<dt className="sr-only">Role</dt>
<dd className="text-xs text-zinc-500 dark:text-zinc-400">
{role.title}
</dd>
<dt className="sr-only">Date</dt>
<dd
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
aria-label={`${role.start.label ?? role.start} until ${
role.end.label ?? role.end
}`}
>
<time dateTime={role.start.dateTime ?? role.start}>
{role.start.label ?? role.start}
</time>{' '}
<span aria-hidden="true"></span>{' '}
<time dateTime={role.end.dateTime ?? role.end}>
{role.end.label ?? role.end}
</time>
</dd>
</dl>
</li>
<Role key={roleIndex} role={role} />
))}
</ol>
<Button
href="/publications"
variant="secondary"
className="group mt-6 w-full"
>
Publications
<ArrowDownIcon className="h-4 w-4 stroke-zinc-400 transition group-active:stroke-zinc-600 dark:group-hover:stroke-zinc-50 dark:group-active:stroke-zinc-50" />
</Button>
{/*
<Button href="#" variant="secondary" className="group mt-6 w-full">
Download CV
<ArrowDownIcon className="h-4 w-4 stroke-zinc-400 transition group-active:stroke-zinc-600 dark:group-hover:stroke-zinc-50 dark:group-active:stroke-zinc-50" />
</Button>
</Button>
*/}
</div>
)
@ -173,18 +180,11 @@ function Photos() {
)
}
export default function Home({ articles }) {
export default async function Home() {
let articles = (await getAllArticles()).slice(0, 4)
return (
<>
<Head>
<title>
Jip J. Dekker - Optimisation Expert & Programming Language Designer
</title>
<meta
name="description"
content="Im Jip, a researcher at the OPTIMA ARC research centre and Monash University, where we aim to make complex decisions easier through decision support and data insights. We design state-of-the-art optimization techniques, such as the MiniZinc modelling language, ready to be used in industry."
/>
</Head>
<Container className="mt-9">
<div className="max-w-2xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
@ -242,17 +242,3 @@ export default function Home({ articles }) {
</>
)
}
export async function getStaticProps() {
if (process.env.NODE_ENV === 'production') {
await generateRssFeed()
}
return {
props: {
articles: (await getAllArticles())
.slice(0, 4)
.map(({ component, ...meta }) => meta),
},
}
}

93
src/app/projects/page.tsx Normal file
View File

@ -0,0 +1,93 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import { LinkIcon } from '@/components/SVGIcons'
import logoChuffed from '@/images/logos/chuffed.png'
import logoMiniZinc from '@/images/logos/minizinc.svg'
import logoMZNPy from '@/images/logos/minizinc-python.svg'
import logoPindakaas from '@/images/logos/pindakaas.svg'
import logoShackle from '@/images/logos/shackle.svg'
const projects = [
{
name: 'MiniZinc',
description:
'A constraint modelling language for almost all types of optimization solvers.',
link: { href: 'http://www.minizinc.org', label: 'minizinc.org' },
logo: logoMiniZinc,
},
{
name: 'Chuffed',
description: 'The solver that brought Lazy Clause Generation to the world.',
link: { href: 'https://github.com/chuffed/chuffed', label: 'github.com' },
logo: logoChuffed,
},
{
name: 'Shackle',
description: 'The next generation of constraint model rewriting tooling.',
link: {
href: 'https://github.com/shackle-rs/shackle',
label: 'github.com',
},
logo: logoShackle,
},
{
name: 'Pindakaas',
description:
'A library that helps you create state-of-the-art encodings for Boolean satisfiability solvers.',
link: { href: '#', label: 'TBA' },
logo: logoPindakaas,
},
{
name: 'MiniZinc Python',
description:
'Easily run MiniZinc from Python, with incremental solving and direct data access.',
link: {
href: 'https://github.com/MiniZinc/minizinc-python',
label: 'github.com',
},
logo: logoMZNPy,
},
]
export const metadata: Metadata = {
title: 'Projects',
description: 'From my brain to your computer',
}
export default function Projects() {
return (
<SimpleLayout
title="From my brain to your computer"
intro="Over the years, I have dedicated myself to many projects, big and small. The following projects are the ones where I feel I had the most impact. They are open-source, providing you with the opportunity to explore them. If you come across something that catches your interest, I encourage you to dive in. Feel free to contact me if you have any questions or would like to collaborate on their development."
>
<ul
role="list"
className="grid grid-cols-1 gap-x-12 gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
>
{projects.map((project) => (
<Card as="li" key={project.name}>
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-xl bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<Image
src={project.logo}
alt=""
className="h-8 w-8"
unoptimized
/>
</div>
<h2 className="mt-6 text-base font-semibold text-zinc-800 dark:text-zinc-100">
<Card.Link href={project.link.href}>{project.name}</Card.Link>
</h2>
<Card.Description>{project.description}</Card.Description>
<p className="relative z-10 mt-6 flex text-sm font-medium text-zinc-400 transition group-hover:text-teal-500 dark:text-zinc-200">
<LinkIcon className="h-6 w-6 flex-none" />
<span className="ml-2">{project.link.label}</span>
</p>
</Card>
))}
</ul>
</SimpleLayout>
)
}

30
src/app/providers.tsx Normal file
View File

@ -0,0 +1,30 @@
'use client'
import { createContext, useEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'
import { ThemeProvider, useTheme } from 'next-themes'
function usePrevious<T>(value: T) {
let ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
export const AppContext = createContext<{ previousPathname?: string }>({})
export function Providers({ children }: { children: React.ReactNode }) {
let pathname = usePathname()
let previousPathname = usePrevious(pathname)
return (
<AppContext.Provider value={{ previousPathname }}>
<ThemeProvider attribute="class" disableTransitionOnChange>
{children}
</ThemeProvider>
</AppContext.Provider>
)
}

98
src/app/speaking/page.tsx Normal file
View File

@ -0,0 +1,98 @@
import { type Metadata } from 'next'
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function SpeakingSection({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Section>) {
return (
<Section {...props}>
<div className="space-y-16">{children}</div>
</Section>
)
}
function Appearance({
title,
description,
event,
cta,
href,
}: {
title: string
description: string
event: string
cta: string
href: string
}) {
return (
<Card as="article">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Eyebrow decorate>{event}</Card.Eyebrow>
<Card.Description>{description}</Card.Description>
<Card.Cta>{cta}</Card.Cta>
</Card>
)
}
export const metadata: Metadata = {
title: 'Speaking',
description:
'Ive spoken at events all around the world and been interviewed for many podcasts.',
}
export default function Speaking() {
return (
<SimpleLayout
title="Ive spoken at events all around the world and been interviewed for many podcasts."
intro="One of my favorite ways to share my ideas is live on stage, where theres so much more communication bandwidth than there is in writing, and I love podcast interviews because they give me the opportunity to answer questions instead of just present my opinions."
>
<div className="space-y-20">
<SpeakingSection title="Conferences">
<Appearance
href="#"
title="In space, no one can watch you stream — until now"
description="A technical deep-dive into HelioStream, the real-time streaming library I wrote for transmitting live video back to Earth."
event="SysConf 2021"
cta="Watch video"
/>
<Appearance
href="#"
title="Lessons learned from our first product recall"
description="They say that if youre not embarassed by your first version, youre doing it wrong. Well when youre selling DIY space shuttle kits it turns out its a bit more complicated."
event="Business of Startups 2020"
cta="Watch video"
/>
</SpeakingSection>
<SpeakingSection title="Podcasts">
<Appearance
href="#"
title="Using design as a competitive advantage"
description="How we used world-class visual design to attract a great team, win over customers, and get more press for Planetaria."
event="Encoding Design, July 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Bootstrapping an aerospace company to $17M ARR"
description="The story of how we built one of the most promising space startups in the world without taking any capital from investors."
event="The Escape Velocity Show, March 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Programming your company operating system"
description="On the importance of creating systems and processes for running your business so that everyone on the team knows how to make the right decision no matter the situation."
event="How They Work Radio, September 2021"
cta="Listen to podcast"
/>
</SpeakingSection>
</div>
</SimpleLayout>
)
}

123
src/app/uses/page.tsx Normal file
View File

@ -0,0 +1,123 @@
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function ToolsSection({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Section>) {
return (
<Section {...props}>
<ul role="list" className="space-y-16">
{children}
</ul>
</Section>
)
}
function Tool({
title,
href,
children,
}: {
title: string
href?: string
children: React.ReactNode
}) {
return (
<Card as="li">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Description>{children}</Card.Description>
</Card>
)
}
export const metadata = {
title: 'Uses',
description: 'Software I use, gadgets I love, and other things I recommend.',
}
export default function Uses() {
return (
<SimpleLayout
title="Software I use, gadgets I love, and other things I recommend."
intro="I get asked a lot about the things I use to build software, stay productive, or buy to fool myself into thinking Im being productive when Im really just procrastinating. Heres a big list of all of my favorite stuff."
>
<div className="space-y-20">
<ToolsSection title="Workstation">
<Tool title="16” MacBook Pro, M1 Max, 64GB RAM (2021)">
I was using an Intel-based 16 MacBook Pro prior to this and the
difference is night and day. Ive never heard the fans turn on a
single time, even under the incredibly heavy loads I put it through
with our various launch simulations.
</Tool>
<Tool title="Apple Pro Display XDR (Standard Glass)">
The only display on the market if you want something HiDPI and
bigger than 27. When youre working at planetary scale, every pixel
you can get counts.
</Tool>
<Tool title="IBM Model M SSK Industrial Keyboard">
They dont make keyboards the way they used to. I buy these any time
I see them go up for sale and keep them in storage in case I need
parts or need to retire my main.
</Tool>
<Tool title="Apple Magic Trackpad">
Something about all the gestures makes me feel like a wizard with
special powers. I really like feeling like a wizard with special
powers.
</Tool>
<Tool title="Herman Miller Aeron Chair">
If Im going to slouch in the worst ergonomic position imaginable
all day, I might as well do it in an expensive chair.
</Tool>
</ToolsSection>
<ToolsSection title="Development tools">
<Tool title="Sublime Text 4">
I dont care if its missing all of the fancy IDE features everyone
else relies on, Sublime Text is still the best text editor ever
made.
</Tool>
<Tool title="iTerm2">
Im honestly not even sure what features I get with this that arent
just part of the macOS Terminal but its what I use.
</Tool>
<Tool title="TablePlus">
Great software for working with databases. Has saved me from
building about a thousand admin interfaces for my various projects
over the years.
</Tool>
</ToolsSection>
<ToolsSection title="Design">
<Tool title="Figma">
We started using Figma as just a design tool but now its become our
virtual whiteboard for the entire company. Never would have expected
the collaboration features to be the real hook.
</Tool>
</ToolsSection>
<ToolsSection title="Productivity">
<Tool title="Alfred">
Its not the newest kid on the block but its still the fastest. The
Sublime Text of the application launcher world.
</Tool>
<Tool title="Reflect">
Using a daily notes system instead of trying to keep things
organized by topics has been super powerful for me. And with
Reflect, its still easy for me to keep all of that stuff
discoverable by topic even though all of my writing happens in the
daily note.
</Tool>
<Tool title="SavvyCal">
Great tool for scheduling meetings while protecting my calendar and
making sure I still have lots of time for deep work during the week.
</Tool>
<Tool title="Focus">
Simple tool for blocking distracting websites when I need to just do
the work and get some momentum going.
</Tool>
</ToolsSection>
</div>
</SimpleLayout>
)
}

View File

@ -1,60 +0,0 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import { Container } from '@/components/Container'
import { Prose } from '@/components/Prose'
import { formatDate } from '@/lib/formatDate'
import { ArrowLeftIcon } from '@/components/SVGIcons'
export function ArticleLayout({
children,
meta,
isRssFeed = false,
previousPathname,
}) {
let router = useRouter()
if (isRssFeed) {
return children
}
return (
<>
<Head>
<title>{`${meta.title} - Jip J. Dekker`}</title>
<meta name="description" content={meta.description} />
</Head>
<Container className="mt-16 lg:mt-32">
<div className="xl:relative">
<div className="mx-auto max-w-2xl">
{previousPathname && (
<button
type="button"
onClick={() => router.back()}
aria-label="Go back to articles"
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-xl bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
>
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />
</button>
)}
<article>
<header className="flex flex-col">
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
{meta.title}
</h1>
<time
dateTime={meta.date}
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
>
<span className="h-4 w-0.5 rounded-xl bg-zinc-200 dark:bg-zinc-500" />
<span className="ml-3">{formatDate(meta.date)}</span>
</time>
</header>
<Prose className="mt-8">{children}</Prose>
</article>
</div>
</div>
</Container>
</>
)
}

View File

@ -0,0 +1,58 @@
'use client'
import { useContext } from 'react'
import { useRouter } from 'next/navigation'
import { AppContext } from '@/app/providers'
import { Container } from '@/components/Container'
import { Prose } from '@/components/Prose'
import { type ArticleWithSlug } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
import { ArrowLeftIcon } from './SVGIcons'
export function ArticleLayout({
article,
children,
}: {
article: ArticleWithSlug
children: React.ReactNode
}) {
let router = useRouter()
let { previousPathname } = useContext(AppContext)
return (
<Container className="mt-16 lg:mt-32">
<div className="xl:relative">
<div className="mx-auto max-w-2xl">
{previousPathname && (
<button
type="button"
onClick={() => router.back()}
aria-label="Go back to articles"
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-xl bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
>
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />
</button>
)}
<article>
<header className="flex flex-col">
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
{article.title}
</h1>
<time
dateTime={article.date}
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
>
<span className="h-4 w-0.5 rounded-xl bg-zinc-200 dark:bg-zinc-500" />
<span className="ml-3">{formatDate(article.date)}</span>
</time>
</header>
<Prose className="mt-8" data-mdx-content>
{children}
</Prose>
</article>
</div>
</div>
</Container>
)
}

View File

@ -8,16 +8,27 @@ const variantStyles = {
'bg-zinc-50 font-medium text-zinc-900 hover:bg-zinc-100 active:bg-zinc-100 active:text-zinc-900/60 dark:bg-zinc-800/50 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:active:bg-zinc-800/50 dark:active:text-zinc-50/70',
}
export function Button({ variant = 'primary', className, href, ...props }) {
type ButtonProps = {
variant?: keyof typeof variantStyles
} & (
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
| React.ComponentPropsWithoutRef<typeof Link>
)
export function Button({
variant = 'primary',
className,
...props
}: ButtonProps) {
className = clsx(
'inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none',
variantStyles[variant],
className,
)
return href ? (
<Link href={href} className={className} {...props} />
) : (
return typeof props.href === 'undefined' ? (
<button className={className} {...props} />
) : (
<Link className={className} {...props} />
)
}

View File

@ -1,9 +1,17 @@
import Link from 'next/link'
import clsx from 'clsx'
import { ChevronRightIcon } from './SVGIcons'
import { ChevronRightIcon } from '@/components/SVGIcons'
export function Card<T extends React.ElementType = 'div'>({
as,
className,
children,
}: Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'className'> & {
as?: T
className?: string
}) {
let Component = as ?? 'div'
export function Card({ as: Component = 'div', className, children }) {
return (
<Component
className={clsx(className, 'group relative flex flex-col items-start')}
@ -13,7 +21,10 @@ export function Card({ as: Component = 'div', className, children }) {
)
}
Card.Link = function CardLink({ children, ...props }) {
Card.Link = function CardLink({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Link>) {
return (
<>
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
@ -25,7 +36,16 @@ Card.Link = function CardLink({ children, ...props }) {
)
}
Card.Title = function CardTitle({ as: Component = 'h2', href, children }) {
Card.Title = function CardTitle<T extends React.ElementType = 'h2'>({
as,
href,
children,
}: Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'href'> & {
as?: T
href?: string
}) {
let Component = as ?? 'h2'
return (
<Component className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
@ -33,7 +53,11 @@ Card.Title = function CardTitle({ as: Component = 'h2', href, children }) {
)
}
Card.Description = function CardDescription({ children }) {
Card.Description = function CardDescription({
children,
}: {
children: React.ReactNode
}) {
return (
<p className="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
{children}
@ -41,7 +65,7 @@ Card.Description = function CardDescription({ children }) {
)
}
Card.Cta = function CardCta({ children }) {
Card.Cta = function CardCta({ children }: { children: React.ReactNode }) {
return (
<div
aria-hidden="true"
@ -53,13 +77,18 @@ Card.Cta = function CardCta({ children }) {
)
}
Card.Eyebrow = function CardEyebrow({
as: Component = 'p',
Card.Eyebrow = function CardEyebrow<T extends React.ElementType = 'p'>({
as,
decorate = false,
className,
children,
...props
}: Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'decorate'> & {
as?: T
decorate?: boolean
}) {
let Component = as ?? 'p'
return (
<Component
className={clsx(

View File

@ -1,42 +0,0 @@
import { forwardRef } from 'react'
import clsx from 'clsx'
const OuterContainer = forwardRef(function OuterContainer(
{ className, children, ...props },
ref,
) {
return (
<div ref={ref} className={clsx('sm:px-8', className)} {...props}>
<div className="mx-auto max-w-7xl lg:px-8">{children}</div>
</div>
)
})
const InnerContainer = forwardRef(function InnerContainer(
{ className, children, ...props },
ref,
) {
return (
<div
ref={ref}
className={clsx('relative px-4 sm:px-8 lg:px-12', className)}
{...props}
>
<div className="mx-auto max-w-2xl lg:max-w-5xl">{children}</div>
</div>
)
})
export const Container = forwardRef(function Container(
{ children, ...props },
ref,
) {
return (
<OuterContainer ref={ref} {...props}>
<InnerContainer>{children}</InnerContainer>
</OuterContainer>
)
})
Container.Outer = OuterContainer
Container.Inner = InnerContainer

View File

@ -0,0 +1,39 @@
import { forwardRef } from 'react'
import clsx from 'clsx'
export const ContainerOuter = forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(function OuterContainer({ className, children, ...props }, ref) {
return (
<div ref={ref} className={clsx('sm:px-8', className)} {...props}>
<div className="mx-auto w-full max-w-7xl lg:px-8">{children}</div>
</div>
)
})
export const ContainerInner = forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(function InnerContainer({ className, children, ...props }, ref) {
return (
<div
ref={ref}
className={clsx('relative px-4 sm:px-8 lg:px-12', className)}
{...props}
>
<div className="mx-auto max-w-2xl lg:max-w-5xl">{children}</div>
</div>
)
})
export const Container = forwardRef<
React.ElementRef<typeof ContainerOuter>,
React.ComponentPropsWithoutRef<typeof ContainerOuter>
>(function Container({ children, ...props }, ref) {
return (
<ContainerOuter ref={ref} {...props}>
<ContainerInner>{children}</ContainerInner>
</ContainerOuter>
)
})

View File

@ -1,9 +1,17 @@
'use client'
import Link from 'next/link'
import { Container } from '@/components/Container'
import { ContainerInner, ContainerOuter } from '@/components/Container'
import { menu_items } from '@/components/Header'
function NavLink({ href, children }) {
function NavLink({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
return (
<Link
href={href}
@ -16,10 +24,10 @@ function NavLink({ href, children }) {
export function Footer() {
return (
<footer className="mt-32">
<Container.Outer>
<footer className="mt-32 flex-none">
<ContainerOuter>
<div className="border-t border-zinc-100 pb-16 pt-10 dark:border-zinc-700/40">
<Container.Inner>
<ContainerInner>
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm font-medium text-zinc-800 dark:text-zinc-200">
{menu_items.map((item) => (
@ -33,9 +41,9 @@ export function Footer() {
reserved.
</p>
</div>
</Container.Inner>
</ContainerInner>
</div>
</Container.Outer>
</ContainerOuter>
</footer>
)
}

View File

@ -1,24 +1,35 @@
'use client'
import { Fragment, useEffect, useRef } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { usePathname } from 'next/navigation'
import { Popover, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { Container } from '@/components/Container'
import avatarImage from '@/images/avatar.jpg'
import { ChevronDownIcon, CloseIcon } from '@/components/SVGIcons'
import { ThemeSelector } from './ThemeSelector'
import { ThemeSelector } from '@/components/ThemeSelector'
import {
ChevronDownIcon,
CloseIcon,
} from '@/components/SVGIcons'
export const menu_items = [
{ name: 'About', url: '/about' },
{ name: 'Articles', url: '/articles' },
{ name: 'Projects', url: '/projects' },
// { name: 'Speaking', url: '/speaking' },
// { name: 'Uses', url: '/uses' },
// { "name": "Speaking", "url": "/speaking" },
// { "name": "Uses", "url": "/uses" },
]
function MobileNavItem({ href, children }) {
function MobileNavItem({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
return (
<li>
<Popover.Button as={Link} href={href} className="block py-2">
@ -28,7 +39,9 @@ function MobileNavItem({ href, children }) {
)
}
function MobileNavigation(props) {
function MobileNavigation(
props: React.ComponentPropsWithoutRef<typeof Popover>,
) {
return (
<Popover {...props}>
<Popover.Button className="group flex items-center rounded-xl bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
@ -84,8 +97,14 @@ function MobileNavigation(props) {
)
}
function NavItem({ href, children }) {
let isActive = useRouter().pathname === href
function NavItem({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
let isActive = usePathname() === href
return (
<li>
@ -107,7 +126,7 @@ function NavItem({ href, children }) {
)
}
function DesktopNavigation(props) {
function DesktopNavigation(props: React.ComponentPropsWithoutRef<'nav'>) {
return (
<nav {...props}>
<ul className="flex rounded-xl bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
@ -121,13 +140,16 @@ function DesktopNavigation(props) {
)
}
function clamp(number, a, b) {
function clamp(number: number, a: number, b: number) {
let min = Math.min(a, b)
let max = Math.max(a, b)
return Math.min(Math.max(number, min), max)
}
function AvatarContainer({ className, ...props }) {
function AvatarContainer({
className,
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div
className={clsx(
@ -139,7 +161,13 @@ function AvatarContainer({ className, ...props }) {
)
}
function Avatar({ large = false, className, ...props }) {
function Avatar({
large = false,
className,
...props
}: Omit<React.ComponentPropsWithoutRef<typeof Link>, 'href'> & {
large?: boolean
}) {
return (
<Link
href="/"
@ -162,25 +190,29 @@ function Avatar({ large = false, className, ...props }) {
}
export function Header() {
let isHomePage = useRouter().pathname === '/'
let isHomePage = usePathname() === '/'
let headerRef = useRef()
let avatarRef = useRef()
let headerRef = useRef<React.ElementRef<'div'>>(null)
let avatarRef = useRef<React.ElementRef<'div'>>(null)
let isInitial = useRef(true)
useEffect(() => {
let downDelay = avatarRef.current?.offsetTop ?? 0
let upDelay = 64
function setProperty(property, value) {
function setProperty(property: string, value: string) {
document.documentElement.style.setProperty(property, value)
}
function removeProperty(property) {
function removeProperty(property: string) {
document.documentElement.style.removeProperty(property)
}
function updateHeaderStyles() {
if (!headerRef.current) {
return
}
let { top, height } = headerRef.current.getBoundingClientRect()
let scrollY = clamp(
window.scrollY,
@ -245,7 +277,7 @@ export function Header() {
let borderTransform = `translate3d(${borderX}rem, 0, 0) scale(${borderScale})`
setProperty('--avatar-border-transform', borderTransform)
setProperty('--avatar-border-opacity', scale === toScale ? 1 : 0)
setProperty('--avatar-border-opacity', scale === toScale ? '1' : '0')
}
function updateStyles() {
@ -267,7 +299,7 @@ export function Header() {
return (
<>
<header
className="pointer-events-none relative z-50 flex flex-col"
className="pointer-events-none relative z-50 flex flex-none flex-col"
style={{
height: 'var(--header-height)',
marginBottom: 'var(--header-mb)',
@ -281,11 +313,17 @@ export function Header() {
/>
<Container
className="top-0 order-last -mb-3 pt-3"
style={{ position: 'var(--header-position)' }}
style={{
position:
'var(--header-position)' as React.CSSProperties['position'],
}}
>
<div
className="top-[var(--avatar-top,theme(spacing.3))] w-full"
style={{ position: 'var(--header-inner-position)' }}
style={{
position:
'var(--header-inner-position)' as React.CSSProperties['position'],
}}
>
<div className="relative">
<AvatarContainer
@ -308,11 +346,17 @@ export function Header() {
<div
ref={headerRef}
className="top-0 z-10 h-16 pt-6"
style={{ position: 'var(--header-position)' }}
style={{
position:
'var(--header-position)' as React.CSSProperties['position'],
}}
>
<Container
className="top-[var(--header-top,theme(spacing.6))] w-full"
style={{ position: 'var(--header-inner-position)' }}
style={{
position:
'var(--header-inner-position)' as React.CSSProperties['position'],
}}
>
<div className="relative flex gap-4">
<div className="flex flex-1">
@ -335,7 +379,12 @@ export function Header() {
</Container>
</div>
</header>
{isHomePage && <div style={{ height: 'var(--content-offset)' }} />}
{isHomePage && (
<div
className="flex-none"
style={{ height: 'var(--content-offset)' }}
/>
)}
</>
)
}

19
src/components/Layout.tsx Normal file
View File

@ -0,0 +1,19 @@
import { Footer } from '@/components/Footer'
import { Header } from '@/components/Header'
export function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<div className="fixed inset-0 flex justify-center sm:px-8">
<div className="flex w-full max-w-7xl lg:px-8">
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
</div>
</div>
<div className="relative flex w-full flex-col">
<Header />
<main className="flex-auto">{children}</main>
<Footer />
</div>
</>
)
}

View File

@ -1,7 +0,0 @@
import clsx from 'clsx'
export function Prose({ children, className }) {
return (
<div className={clsx(className, 'prose dark:prose-invert')}>{children}</div>
)
}

10
src/components/Prose.tsx Normal file
View File

@ -0,0 +1,10 @@
import clsx from 'clsx'
export function Prose({
className,
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div className={clsx(className, 'prose dark:prose-invert')} {...props} />
)
}

View File

@ -1,4 +1,4 @@
export function ArrowDownIcon(props) {
export function ArrowDownIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
@ -11,7 +11,7 @@ export function ArrowDownIcon(props) {
)
}
export function ArrowLeftIcon(props) {
export function ArrowLeftIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
@ -24,7 +24,7 @@ export function ArrowLeftIcon(props) {
)
}
export function BriefcaseIcon(props) {
export function BriefcaseIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
@ -47,7 +47,7 @@ export function BriefcaseIcon(props) {
)
}
export function ChevronDownIcon(props) {
export function ChevronDownIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 8 6" aria-hidden="true" {...props}>
<path
@ -61,7 +61,7 @@ export function ChevronDownIcon(props) {
)
}
export function ChevronRightIcon(props) {
export function ChevronRightIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
@ -74,7 +74,7 @@ export function ChevronRightIcon(props) {
)
}
export function CloseIcon(props) {
export function CloseIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
@ -89,7 +89,7 @@ export function CloseIcon(props) {
)
}
export function GitHubIcon(props) {
export function GitHubIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
@ -101,7 +101,7 @@ export function GitHubIcon(props) {
)
}
export function InstagramIcon(props) {
export function InstagramIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M12 3c-2.444 0-2.75.01-3.71.054-.959.044-1.613.196-2.185.418A4.412 4.412 0 0 0 4.51 4.511c-.5.5-.809 1.002-1.039 1.594-.222.572-.374 1.226-.418 2.184C3.01 9.25 3 9.556 3 12s.01 2.75.054 3.71c.044.959.196 1.613.418 2.185.23.592.538 1.094 1.039 1.595.5.5 1.002.808 1.594 1.038.572.222 1.226.374 2.184.418C9.25 20.99 9.556 21 12 21s2.75-.01 3.71-.054c.959-.044 1.613-.196 2.185-.419a4.412 4.412 0 0 0 1.595-1.038c.5-.5.808-1.002 1.038-1.594.222-.572.374-1.226.418-2.184.044-.96.054-1.267.054-3.711s-.01-2.75-.054-3.71c-.044-.959-.196-1.613-.419-2.185A4.412 4.412 0 0 0 19.49 4.51c-.5-.5-1.002-.809-1.594-1.039-.572-.222-1.226-.374-2.184-.418C14.75 3.01 14.444 3 12 3Zm0 1.622c2.403 0 2.688.009 3.637.052.877.04 1.354.187 1.67.31.421.163.72.358 1.036.673.315.315.51.615.673 1.035.123.317.27.794.31 1.671.043.95.052 1.234.052 3.637s-.009 2.688-.052 3.637c-.04.877-.187 1.354-.31 1.67-.163.421-.358.72-.673 1.036a2.79 2.79 0 0 1-1.035.673c-.317.123-.794.27-1.671.31-.95.043-1.234.052-3.637.052s-2.688-.009-3.637-.052c-.877-.04-1.354-.187-1.67-.31a2.789 2.789 0 0 1-1.036-.673 2.79 2.79 0 0 1-.673-1.035c-.123-.317-.27-.794-.31-1.671-.043-.95-.052-1.234-.052-3.637s.009-2.688.052-3.637c.04-.877.187-1.354.31-1.67.163-.421.358-.72.673-1.036.315-.315.615-.51 1.035-.673.317-.123.794-.27 1.671-.31.95-.043 1.234-.052 3.637-.052Z" />
@ -110,7 +110,7 @@ export function InstagramIcon(props) {
)
}
export function LinkedInIcon(props) {
export function LinkedInIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M18.335 18.339H15.67v-4.177c0-.996-.02-2.278-1.39-2.278-1.389 0-1.601 1.084-1.601 2.205v4.25h-2.666V9.75h2.56v1.17h.035c.358-.674 1.228-1.387 2.528-1.387 2.7 0 3.2 1.778 3.2 4.091v4.715zM7.003 8.575a1.546 1.546 0 01-1.548-1.549 1.548 1.548 0 111.547 1.549zm1.336 9.764H5.666V9.75H8.34v8.589zM19.67 3H4.329C3.593 3 3 3.58 3 4.297v15.406C3 20.42 3.594 21 4.328 21h15.338C20.4 21 21 20.42 21 19.703V4.297C21 3.58 20.4 3 19.666 3h.003z" />
@ -118,7 +118,7 @@ export function LinkedInIcon(props) {
)
}
export function LinkIcon(props) {
export function LinkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
@ -129,7 +129,7 @@ export function LinkIcon(props) {
)
}
export function MailIcon(props) {
export function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
@ -152,7 +152,7 @@ export function MailIcon(props) {
)
}
export function MastodonIcon(props) {
export function MastodonIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 512 512" aria-hidden="true" {...props}>
<path d="M480,173.59c0-104.13-68.26-134.65-68.26-134.65C377.3,23.15,318.2,16.5,256.8,16h-1.51c-61.4.5-120.46,7.15-154.88,22.94,0,0-68.27,30.52-68.27,134.65,0,23.85-.46,52.35.29,82.59C34.91,358,51.11,458.37,145.32,483.29c43.43,11.49,80.73,13.89,110.76,12.24,54.47-3,85-19.42,85-19.42l-1.79-39.5s-38.93,12.27-82.64,10.77c-43.31-1.48-89-4.67-96-57.81a108.44,108.44,0,0,1-1-14.9,558.91,558.91,0,0,0,96.39,12.85c32.95,1.51,63.84-1.93,95.22-5.67,60.18-7.18,112.58-44.24,119.16-78.09C480.84,250.42,480,173.59,480,173.59ZM399.46,307.75h-50V185.38c0-25.8-10.86-38.89-32.58-38.89-24,0-36.06,15.53-36.06,46.24v67H231.16v-67c0-30.71-12-46.24-36.06-46.24-21.72,0-32.58,13.09-32.58,38.89V307.75h-50V181.67q0-38.65,19.75-61.39c13.6-15.15,31.4-22.92,53.51-22.92,25.58,0,44.95,9.82,57.75,29.48L256,147.69l12.45-20.85c12.81-19.66,32.17-29.48,57.75-29.48,22.11,0,39.91,7.77,53.51,22.92Q399.5,143,399.46,181.67Z" />
@ -160,7 +160,7 @@ export function MastodonIcon(props) {
)
}
export function MoonIcon(props) {
export function MoonIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
@ -173,7 +173,7 @@ export function MoonIcon(props) {
)
}
export function ORCIDIcon(props) {
export function ORCIDIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 512 512" aria-hidden="true" {...props}>
<path d="M294.75 188.19h-45.92V342h47.47c67.62 0 83.12-51.34 83.12-76.91 0-41.64-26.54-76.9-84.67-76.9zM256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm-80.79 360.76h-29.84v-207.5h29.84zm-14.92-231.14a19.57 19.57 0 1 1 19.57-19.57 19.64 19.64 0 0 1-19.57 19.57zM300 369h-81V161.26h80.6c76.73 0 110.44 54.83 110.44 103.85C410 318.39 368.38 369 300 369z"></path>
@ -181,7 +181,7 @@ export function ORCIDIcon(props) {
)
}
export function ResearchGateIcon(props) {
export function ResearchGateIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 448 512" aria-hidden="true" {...props}>
<path d="M0 32v448h448V32H0zm262.2 334.4c-6.6 3-33.2 6-50-14.2-9.2-10.6-25.3-33.3-42.2-63.6-8.9 0-14.7 0-21.4-.6v46.4c0 23.5 6 21.2 25.8 23.9v8.1c-6.9-.3-23.1-.8-35.6-.8-13.1 0-26.1.6-33.6.8v-8.1c15.5-2.9 22-1.3 22-23.9V225c0-22.6-6.4-21-22-23.9V193c25.8 1 53.1-.6 70.9-.6 31.7 0 55.9 14.4 55.9 45.6 0 21.1-16.7 42.2-39.2 47.5 13.6 24.2 30 45.6 42.2 58.9 7.2 7.8 17.2 14.7 27.2 14.7v7.3zm22.9-135c-23.3 0-32.2-15.7-32.2-32.2V167c0-12.2 8.8-30.4 34-30.4s30.4 17.9 30.4 17.9l-10.7 7.2s-5.5-12.5-19.7-12.5c-7.9 0-19.7 7.3-19.7 19.7v26.8c0 13.4 6.6 23.3 17.9 23.3 14.1 0 21.5-10.9 21.5-26.8h-17.9v-10.7h30.4c0 20.5 4.7 49.9-34 49.9zm-116.5 44.7c-9.4 0-13.6-.3-20-.8v-69.7c6.4-.6 15-.6 22.5-.6 23.3 0 37.2 12.2 37.2 34.5 0 21.9-15 36.6-39.7 36.6z"></path>
@ -189,15 +189,7 @@ export function ResearchGateIcon(props) {
)
}
export function TwitterIcon(props) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M20.055 7.983c.011.174.011.347.011.523 0 5.338-3.92 11.494-11.09 11.494v-.003A10.755 10.755 0 0 1 3 18.186c.308.038.618.057.928.058a7.655 7.655 0 0 0 4.841-1.733c-1.668-.032-3.13-1.16-3.642-2.805a3.753 3.753 0 0 0 1.76-.07C5.07 13.256 3.76 11.6 3.76 9.676v-.05a3.77 3.77 0 0 0 1.77.505C3.816 8.945 3.288 6.583 4.322 4.737c1.98 2.524 4.9 4.058 8.034 4.22a4.137 4.137 0 0 1 1.128-3.86A3.807 3.807 0 0 1 19 5.274a7.657 7.657 0 0 0 2.475-.98c-.29.934-.9 1.729-1.713 2.233A7.54 7.54 0 0 0 22 5.89a8.084 8.084 0 0 1-1.945 2.093Z" />
</svg>
)
}
export function SunIcon(props) {
export function SunIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
@ -216,7 +208,7 @@ export function SunIcon(props) {
)
}
export function SystemIcon(props) {
export function SystemIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path
@ -227,3 +219,11 @@ export function SystemIcon(props) {
</svg>
)
}
export function TwitterIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M20.055 7.983c.011.174.011.347.011.523 0 5.338-3.92 11.494-11.09 11.494v-.003A10.755 10.755 0 0 1 3 18.186c.308.038.618.057.928.058a7.655 7.655 0 0 0 4.841-1.733c-1.668-.032-3.13-1.16-3.642-2.805a3.753 3.753 0 0 0 1.76-.07C5.07 13.256 3.76 11.6 3.76 9.676v-.05a3.77 3.77 0 0 0 1.77.505C3.816 8.945 3.288 6.583 4.322 4.737c1.98 2.524 4.9 4.058 8.034 4.22a4.137 4.137 0 0 1 1.128-3.86A3.807 3.807 0 0 1 19 5.274a7.657 7.657 0 0 0 2.475-.98c-.29.934-.9 1.729-1.713 2.233A7.54 7.54 0 0 0 22 5.89a8.084 8.084 0 0 1-1.945 2.093Z" />
</svg>
)
}

View File

@ -1,6 +1,12 @@
import { useId } from 'react'
export function Section({ title, children }) {
export function Section({
title,
children,
}: {
title: string
children: React.ReactNode
}) {
let id = useId()
return (

View File

@ -1,6 +1,14 @@
import { Container } from '@/components/Container'
export function SimpleLayout({ title, intro, children }) {
export function SimpleLayout({
title,
intro,
children,
}: {
title: string
intro: string
children?: React.ReactNode
}) {
return (
<Container className="mt-16 sm:mt-32">
<header className="max-w-2xl">
@ -11,7 +19,7 @@ export function SimpleLayout({ title, intro, children }) {
{intro}
</p>
</header>
<div className="mt-16 sm:mt-20">{children}</div>
{children && <div className="mt-16 sm:mt-20">{children}</div>}
</Container>
)
}

View File

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
import { Listbox } from '@headlessui/react'
import clsx from 'clsx'
import { MoonIcon, SunIcon, SystemIcon } from '@/components//SVGIcons'
import { MoonIcon, SunIcon, SystemIcon } from './SVGIcons'
const themes = [
{ name: 'Light', value: 'light', icon: SunIcon },
@ -10,62 +10,50 @@ const themes = [
{ name: 'System', value: 'system', icon: SystemIcon },
]
export function ThemeSelector(props) {
let [selectedTheme, setSelectedTheme] = useState(null)
export function ThemeSelector(
props: React.ComponentPropsWithoutRef<typeof Listbox<'div'>>,
) {
let { theme, setTheme } = useTheme()
let [mounted, setMounted] = useState(false)
useEffect(() => {
if (selectedTheme) {
document.documentElement.setAttribute('data-theme', selectedTheme.value)
} else {
setSelectedTheme(
themes.find(
(theme) =>
theme.value === document.documentElement.getAttribute('data-theme'),
),
)
}
}, [selectedTheme])
useEffect(() => {
let handler = () =>
setSelectedTheme(
themes.find(
(theme) => theme.value === (window.localStorage.theme ?? 'system'),
),
)
window.addEventListener('storage', handler)
return () => window.removeEventListener('storage', handler)
setMounted(true)
}, [])
if (!mounted) {
return <div className="h-8 w-8" />
}
return (
<Listbox
as="div"
value={selectedTheme}
onChange={setSelectedTheme}
{...props}
>
<Listbox as="div" value={theme} onChange={setTheme} {...props}>
<Listbox.Label className="sr-only">Theme</Listbox.Label>
<Listbox.Button
className="group rounded-xl bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
aria-label={selectedTheme?.name}
className="flex h-10 w-10 items-center justify-center rounded-xl shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-zinc-700 dark:ring-inset dark:ring-white/5"
aria-label="Theme"
>
<SunIcon className="hidden h-6 w-6 fill-teal-50 stroke-teal-500 [[data-theme=light]_&]:block" />
<MoonIcon className="hidden h-6 w-6 fill-teal-400/10 stroke-teal-500 [[data-theme=dark]_&]:block" />
<SunIcon className="hidden h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 [:not(.dark)[data-theme=system]_&]:block" />
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition group-hover:stroke-zinc-400 [.dark[data-theme=system]_&]:block" />
<SunIcon
className={clsx(
'h-6 w-6 dark:hidden',
theme === 'system' ? 'fill-zinc-400 stroke-zinc-400' : 'fill-teal-400 stroke-teal-400',
)}
/>
<MoonIcon
className={clsx(
'hidden h-6 w-6 dark:block',
theme === 'system' ? 'fill-zinc-400 stroke-zinc-400' : 'fill-teal-400 stroke-teal-400',
)}
/>
</Listbox.Button>
<Listbox.Options className="absolute left-1/2 top-full mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-zinc-800 dark:ring-white/5">
{themes.map((theme) => (
<Listbox.Option
key={theme.value}
value={theme}
value={theme.value}
className={({ active, selected }) =>
clsx(
'flex cursor-pointer select-none items-center rounded-[0.625rem] p-1',
{
'text-teal-400': selected,
'text-teal-500': selected,
'text-zinc-900 dark:text-white': active && !selected,
'text-zinc-700 dark:text-zinc-400': !active && !selected,
'bg-zinc-100 dark:bg-zinc-900/40': active,
@ -81,7 +69,7 @@ export function ThemeSelector(props) {
'h-4 w-4',
selected
? 'fill-teal-400 stroke-teal-400'
: 'fill-zinc-500 stroke-zinc-500',
: 'fill-zinc-400 stroke-zinc-400',
)}
/>
</div>

36
src/lib/articles.ts Normal file
View File

@ -0,0 +1,36 @@
import glob from 'fast-glob'
interface Article {
title: string
description: string
author: string
date: string
}
export interface ArticleWithSlug extends Article {
slug: string
}
async function importArticle(
articleFilename: string,
): Promise<ArticleWithSlug> {
let { article } = (await import(`../app/articles/${articleFilename}`)) as {
default: React.ComponentType
article: Article
}
return {
slug: articleFilename.replace(/(\/page)?\.mdx$/, ''),
...article,
}
}
export async function getAllArticles() {
let articleFilenames = await glob('*/page.mdx', {
cwd: './src/app/articles',
})
let articles = await Promise.all(articleFilenames.map(importArticle))
return articles.sort((a, z) => +new Date(z.date) - +new Date(a.date))
}

View File

@ -1,4 +1,4 @@
export function formatDate(dateString) {
export function formatDate(dateString: string) {
return new Date(`${dateString}T00:00:00Z`).toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',

View File

@ -1,57 +0,0 @@
import ReactDOMServer from 'react-dom/server'
import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider'
import { Feed } from 'feed'
import { mkdir, writeFile } from 'fs/promises'
import { getAllArticles } from './getAllArticles'
export async function generateRssFeed() {
let articles = await getAllArticles()
let siteUrl = process.env.NEXT_PUBLIC_SITE_URL
let author = {
name: 'Jip J. Dekker',
email: 'jip.dekker@monash.edu',
}
let feed = new Feed({
title: author.name,
description:
'The collection of writing by Jip about optimization, programming language, and general computer science',
author,
id: siteUrl,
link: siteUrl,
image: `${siteUrl}/favicon.ico`,
favicon: `${siteUrl}/favicon.ico`,
copyright: `All rights reserved ${new Date().getFullYear()}`,
feedLinks: {
rss2: `${siteUrl}/rss/feed.xml`,
json: `${siteUrl}/rss/feed.json`,
},
})
for (let article of articles) {
let url = `${siteUrl}/articles/${article.slug}`
let html = ReactDOMServer.renderToStaticMarkup(
<MemoryRouterProvider>
<article.component isRssFeed />
</MemoryRouterProvider>,
)
feed.addItem({
title: article.title,
id: url,
link: url,
description: article.description,
content: html,
author: [author],
contributor: [author],
date: new Date(article.date),
})
}
await mkdir('./public/rss', { recursive: true })
await Promise.all([
writeFile('./public/rss/feed.xml', feed.rss2(), 'utf8'),
writeFile('./public/rss/feed.json', feed.json1(), 'utf8'),
])
}

View File

@ -1,23 +0,0 @@
import glob from 'fast-glob'
import * as path from 'path'
async function importArticle(articleFilename) {
let { meta, default: component } = await import(
`../pages/articles/${articleFilename}`
)
return {
slug: articleFilename.replace(/(\/index)?\.mdx$/, ''),
...meta,
component,
}
}
export async function getAllArticles() {
let articleFilenames = await glob(['*.mdx', '*/index.mdx'], {
cwd: path.join(process.cwd(), 'src/pages/articles'),
})
let articles = await Promise.all(articleFilenames.map(importArticle))
return articles.sort((a, z) => new Date(z.date) - new Date(a.date))
}

View File

@ -1,38 +0,0 @@
import { useEffect, useRef } from 'react'
import { Footer } from '@/components/Footer'
import { Header } from '@/components/Header'
import '@/styles/tailwind.css'
import 'focus-visible'
function usePrevious(value) {
let ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
export default function App({ Component, pageProps, router }) {
let previousPathname = usePrevious(router.pathname)
return (
<>
<div className="fixed inset-0 flex justify-center sm:px-8">
<div className="flex w-full max-w-7xl lg:px-8">
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
</div>
</div>
<div className="relative">
<Header />
<main>
<Component previousPathname={previousPathname} {...pageProps} />
</main>
<Footer />
</div>
</>
)
}

View File

@ -1,63 +0,0 @@
import { Head, Html, Main, NextScript } from 'next/document'
const themeScript = `
let isDarkMode = window.matchMedia('(prefers-color-scheme: dark)')
function updateTheme(theme) {
theme = theme ?? window.localStorage.theme ?? 'system'
if (theme === 'dark' || (theme === 'system' && isDarkMode.matches)) {
document.documentElement.classList.add('dark')
} else if (theme === 'light' || (theme === 'system' && !isDarkMode.matches)) {
document.documentElement.classList.remove('dark')
}
return theme
}
function updateThemeWithoutTransitions(theme) {
updateTheme(theme)
document.documentElement.classList.add('[&_*]:!transition-none')
window.setTimeout(() => {
document.documentElement.classList.remove('[&_*]:!transition-none')
}, 0)
}
document.documentElement.setAttribute('data-theme', updateTheme())
new MutationObserver(([{ oldValue }]) => {
let newValue = document.documentElement.getAttribute('data-theme')
if (newValue !== oldValue) {
try {
window.localStorage.setItem('theme', newValue)
} catch {}
updateThemeWithoutTransitions(newValue)
}
}).observe(document.documentElement, { attributeFilter: ['data-theme'], attributeOldValue: true })
isDarkMode.addEventListener('change', () => updateThemeWithoutTransitions())
`
export default function Document() {
return (
<Html className="h-full antialiased" lang="en">
<Head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<link
rel="alternate"
type="application/rss+xml"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/rss/feed.xml`}
/>
<link
rel="alternate"
type="application/feed+json"
href={`${process.env.NEXT_PUBLIC_SITE_URL}/rss/feed.json`}
/>
</Head>
<body className="flex h-full flex-col bg-zinc-50 dark:bg-black">
<Main />
<NextScript />
</body>
</Html>
)
}

View File

@ -1,155 +0,0 @@
import Head from 'next/head'
import Image from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { Container } from '@/components/Container'
import {
GitHubIcon,
LinkedInIcon,
MastodonIcon,
ORCIDIcon,
ResearchGateIcon,
} from '@/components/SVGIcons'
import portraitImage from '@/images/portrait.jpg'
function SocialLink({ className, href, children, icon: Icon }) {
return (
<li className={clsx(className, 'flex')}>
<Link
href={href}
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
>
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
<span className="ml-4">{children}</span>
</Link>
</li>
)
}
function MailIcon(props) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
d="M6 5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6Zm.245 2.187a.75.75 0 0 0-.99 1.126l6.25 5.5a.75.75 0 0 0 .99 0l6.25-5.5a.75.75 0 0 0-.99-1.126L12 12.251 6.245 7.187Z"
/>
</svg>
)
}
export default function About() {
return (
<>
<Head>
<title>About - Jip J. Dekker</title>
<meta
name="description"
content="Im dr. Jip J. Dekker, a problem solver using optimization technologies."
/>
</Head>
<Container className="mt-16 sm:mt-32">
<div className="grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
<div className="lg:pl-20">
<div className="max-w-xs px-2.5 lg:max-w-none">
<Image
src={portraitImage}
alt=""
sizes="(min-width: 1024px) 32rem, 20rem"
className="aspect-square rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
/>
</div>
</div>
<div className="lg:order-first lg:row-span-2">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
Im dr. Jip J. Dekker, a problem solver using optimization
technologies.
</h1>
<div className="mt-6 space-y-7 text-base text-zinc-600 dark:text-zinc-400">
<p>
Im a computer scientist at Monash University specializing in
optimization and devoted to developing cutting-edge tools and
modelling languages that simplify the process of solving complex
problems. With a passion for advancing the field, I contribute
to the optimization field, enhancing its efficiency, and
facilitating its practical application.
</p>
<p>
Building from a foundation in constraint programming, I have
gained experience in Boolean satisfiability, mathematical
modelling, local search, various hybrid method. With the wide
range of optimization technologies at my fingertips, it enables
me to approach optimization challenges from many angles. This
allows me to facilitate the creation of innovative software
solutions that streamline the formulation, solution, and
analysis of optimization problems.
</p>
<p>
At the heart of my work is a commitment to making optimization
more accessible and user-friendly. By developing intuitive tools
and modelling languages, I want to empower researchers,
practitioners, and anyone willing to learn to effectively
address complex optimization challenges across a wide range of
domains. I hope that my work not only enhances the effectiveness
of optimization technologies, but also fosters innovation and
improvements in other industries.
</p>
<p>
When Im not solving optimization problems, I love bird
watching, music, and cycling. The beauty of the avian world has
captivated me ever since I moved to Australia. Music brings me
solace. Im always looking for new songs with interesting
melodies or striking lyrics. Cycling always gives me a sense of
freedom. You find the most wonderful places, if you stray from
the beaten track even slightly. These pursuits bring me joy and
inspiration.
</p>
</div>
</div>
<div className="lg:pl-20">
<ul role="list">
<SocialLink href="https://github.com/Dekker1" icon={GitHubIcon}>
Follow me on GitHub
</SocialLink>
<SocialLink
href="https://soapbox.network/@Dekker1"
icon={MastodonIcon}
className="mt-4"
>
Follow me on Mastodon
</SocialLink>
<SocialLink
href="https://www.linkedin.com/in/dekker1/"
icon={LinkedInIcon}
className="mt-4"
>
Connect with me on LinkedIn
</SocialLink>
<SocialLink
href="https://orcid.org/0000-0002-0053-6724"
icon={ORCIDIcon}
className="mt-4"
>
Find me on ORCID
</SocialLink>
<SocialLink
href="https://www.researchgate.net/profile/Jip-Dekker-2"
icon={ResearchGateIcon}
className="mt-4"
>
Follow me on ResearchGate
</SocialLink>
<SocialLink
href="mailto:jip.dekker@monash.edu"
icon={MailIcon}
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
>
jip.dekker@monash.edu
</SocialLink>
</ul>
</div>
</div>
</Container>
</>
)
}

View File

@ -1,69 +0,0 @@
import Head from 'next/head'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import { formatDate } from '@/lib/formatDate'
import { getAllArticles } from '@/lib/getAllArticles'
function Article({ article }) {
return (
<article className="md:grid md:grid-cols-4 md:items-baseline">
<Card className="md:col-span-3">
<Card.Title href={`/articles/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="md:hidden"
decorate
>
{formatDate(article.date)}
</Card.Eyebrow>
<Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta>
</Card>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="mt-1 hidden md:block"
>
{formatDate(article.date)}
</Card.Eyebrow>
</article>
)
}
export default function ArticlesIndex({ articles }) {
return (
<>
<Head>
<title>Articles - Jip J. Dekker</title>
<meta
name="description"
content="All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order."
/>
</Head>
<SimpleLayout
title="Writing on optimization, programming languages."
intro="All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order."
>
<div className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
<div className="flex max-w-3xl flex-col space-y-16">
{articles.map((article) => (
<Article key={article.slug} article={article} />
))}
</div>
</div>
</SimpleLayout>
</>
)
}
export async function getStaticProps() {
return {
props: {
articles: (await getAllArticles()).map(({ component, ...meta }) => meta),
},
}
}

View File

@ -1,94 +0,0 @@
import Head from 'next/head'
import Image from 'next/image'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import logoChuffed from '@/images/logos/chuffed.png'
import logoMiniZinc from '@/images/logos/minizinc.svg'
import logoMZNPy from '@/images/logos/minizinc-python.svg'
import logoPindakaas from '@/images/logos/pindakaas.svg'
import logoShackle from '@/images/logos/shackle.svg'
import { LinkIcon } from '@/components/SVGIcons'
const projects = [
{
name: 'MiniZinc',
description:
'A constraint modelling language for almost all types of optimization solvers.',
link: { href: 'http://www.minizinc.org', label: 'minizinc.org' },
logo: logoMiniZinc,
},
{
name: 'Chuffed',
description: 'The solver that brought Lazy Clause Generation to the world.',
link: { href: 'https://github.com/chuffed/chuffed', label: 'github.com' },
logo: logoChuffed,
},
{
name: 'Shackle',
description: 'The next generation of constraint model rewriting tooling.',
link: {
href: 'https://github.com/shackle-rs/shackle',
label: 'github.com',
},
logo: logoShackle,
},
{
name: 'Pindakaas',
description:
'A library that helps you create state-of-the-art encodings for Boolean satisfiability solvers.',
link: { href: '#', label: 'TBA' },
logo: logoPindakaas,
},
{
name: 'MiniZinc Python',
description:
'Easily run MiniZinc from Python, with incremental solving and direct data access.',
link: {
href: 'https://github.com/MiniZinc/minizinc-python',
label: 'github.com',
},
logo: logoMZNPy,
},
]
export default function Projects() {
return (
<>
<Head>
<title>Projects - Jip J. Dekker</title>
<meta name="description" content="From my brain to your computer" />
</Head>
<SimpleLayout
title="From my brain to your computer"
intro="Over the years, I have dedicated myself to many projects, big and small. The following projects are the ones where I feel I had the most impact. They are open-source, providing you with the opportunity to explore them. If you come across something that catches your interest, I encourage you to dive in. Feel free to contact me if you have any questions or would like to collaborate on their development."
>
<ul
role="list"
className="grid grid-cols-1 gap-x-12 gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
>
{projects.map((project) => (
<Card as="li" key={project.name}>
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-xl bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<Image
src={project.logo}
alt=""
className="h-8 w-8"
unoptimized
/>
</div>
<h2 className="mt-6 text-base font-semibold text-zinc-800 dark:text-zinc-100">
<Card.Link href={project.link.href}>{project.name}</Card.Link>
</h2>
<Card.Description>{project.description}</Card.Description>
<p className="relative z-10 mt-6 flex text-sm font-medium text-zinc-400 transition group-hover:text-teal-500 dark:text-zinc-200">
<LinkIcon className="h-6 w-6 flex-none" />
<span className="ml-2">{project.link.label}</span>
</p>
</Card>
))}
</ul>
</SimpleLayout>
</>
)
}

View File

@ -1,86 +0,0 @@
import Head from 'next/head'
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function SpeakingSection({ children, ...props }) {
return (
<Section {...props}>
<div className="space-y-16">{children}</div>
</Section>
)
}
function Appearance({ title, description, event, cta, href }) {
return (
<Card as="article">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Eyebrow decorate>{event}</Card.Eyebrow>
<Card.Description>{description}</Card.Description>
<Card.Cta>{cta}</Card.Cta>
</Card>
)
}
export default function Speaking() {
return (
<>
<Head>
<title>Speaking - Spencer Sharp</title>
<meta
name="description"
content="Ive spoken at events all around the world and been interviewed for many podcasts."
/>
</Head>
<SimpleLayout
title="Ive spoken at events all around the world and been interviewed for many podcasts."
intro="One of my favorite ways to share my ideas is live on stage, where theres so much more communication bandwidth than there is in writing, and I love podcast interviews because they give me the opportunity to answer questions instead of just present my opinions."
>
<div className="space-y-20">
<SpeakingSection title="Conferences">
<Appearance
href="#"
title="In space, no one can watch you stream — until now"
description="A technical deep-dive into HelioStream, the real-time streaming library I wrote for transmitting live video back to Earth."
event="SysConf 2021"
cta="Watch video"
/>
<Appearance
href="#"
title="Lessons learned from our first product recall"
description="They say that if youre not embarassed by your first version, youre doing it wrong. Well when youre selling DIY space shuttle kits it turns out its a bit more complicated."
event="Business of Startups 2020"
cta="Watch video"
/>
</SpeakingSection>
<SpeakingSection title="Podcasts">
<Appearance
href="#"
title="Using design as a competitive advantage"
description="How we used world-class visual design to attract a great team, win over customers, and get more press for Planetaria."
event="Encoding Design, July 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Bootstrapping an aerospace company to $17M ARR"
description="The story of how we built one of the most promising space startups in the world without taking any capital from investors."
event="The Escape Velocity Show, March 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Programming your company operating system"
description="On the importance of creating systems and processes for running your business so that everyone on the team knows how to make the right decision no matter the situation."
event="How They Work Radio, September 2021"
cta="Listen to podcast"
/>
</SpeakingSection>
</div>
</SimpleLayout>
</>
)
}

View File

@ -1,119 +0,0 @@
import Head from 'next/head'
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function ToolsSection({ children, ...props }) {
return (
<Section {...props}>
<ul role="list" className="space-y-16">
{children}
</ul>
</Section>
)
}
function Tool({ title, href, children }) {
return (
<Card as="li">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Description>{children}</Card.Description>
</Card>
)
}
export default function Uses() {
return (
<>
<Head>
<title>Uses - Spencer Sharp</title>
<meta
name="description"
content="Software I use, gadgets I love, and other things I recommend."
/>
</Head>
<SimpleLayout
title="Software I use, gadgets I love, and other things I recommend."
intro="I get asked a lot about the things I use to build software, stay productive, or buy to fool myself into thinking Im being productive when Im really just procrastinating. Heres a big list of all of my favorite stuff."
>
<div className="space-y-20">
<ToolsSection title="Workstation">
<Tool title="16” MacBook Pro, M1 Max, 64GB RAM (2021)">
I was using an Intel-based 16 MacBook Pro prior to this and the
difference is night and day. Ive never heard the fans turn on a
single time, even under the incredibly heavy loads I put it
through with our various launch simulations.
</Tool>
<Tool title="Apple Pro Display XDR (Standard Glass)">
The only display on the market if you want something HiDPI and
bigger than 27. When youre working at planetary scale, every
pixel you can get counts.
</Tool>
<Tool title="IBM Model M SSK Industrial Keyboard">
They dont make keyboards the way they used to. I buy these any
time I see them go up for sale and keep them in storage in case I
need parts or need to retire my main.
</Tool>
<Tool title="Apple Magic Trackpad">
Something about all the gestures makes me feel like a wizard with
special powers. I really like feeling like a wizard with special
powers.
</Tool>
<Tool title="Herman Miller Aeron Chair">
If Im going to slouch in the worst ergonomic position imaginable
all day, I might as well do it in an expensive chair.
</Tool>
</ToolsSection>
<ToolsSection title="Development tools">
<Tool title="Sublime Text 4">
I dont care if its missing all of the fancy IDE features
everyone else relies on, Sublime Text is still the best text
editor ever made.
</Tool>
<Tool title="iTerm2">
Im honestly not even sure what features I get with this that
arent just part of the macOS Terminal but its what I use.
</Tool>
<Tool title="TablePlus">
Great software for working with databases. Has saved me from
building about a thousand admin interfaces for my various projects
over the years.
</Tool>
</ToolsSection>
<ToolsSection title="Design">
<Tool title="Figma">
We started using Figma as just a design tool but now its become
our virtual whiteboard for the entire company. Never would have
expected the collaboration features to be the real hook.
</Tool>
</ToolsSection>
<ToolsSection title="Productivity">
<Tool title="Alfred">
Its not the newest kid on the block but its still the fastest.
The Sublime Text of the application launcher world.
</Tool>
<Tool title="Reflect">
Using a daily notes system instead of trying to keep things
organized by topics has been super powerful for me. And with
Reflect, its still easy for me to keep all of that stuff
discoverable by topic even though all of my writing happens in the
daily note.
</Tool>
<Tool title="SavvyCal">
Great tool for scheduling meetings while protecting my calendar
and making sure I still have lots of time for deep work during the
week.
</Tool>
<Tool title="Focus">
Simple tool for blocking distracting websites when I need to just
do the work and get some momentum going.
</Tool>
</ToolsSection>
</div>
</SimpleLayout>
</>
)
}

View File

@ -1,306 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx}'],
darkMode: 'class',
plugins: [
require('@tailwindcss/typography'),
require('@catppuccin/tailwindcss'),
],
theme: {
fontSize: {
xs: ['0.8125rem', { lineHeight: '1.5rem' }],
sm: ['0.875rem', { lineHeight: '1.5rem' }],
base: ['1rem', { lineHeight: '1.75rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '2rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2rem', { lineHeight: '2.5rem' }],
'5xl': ['3rem', { lineHeight: '3.5rem' }],
'6xl': ['3.75rem', { lineHeight: '1' }],
'7xl': ['4.5rem', { lineHeight: '1' }],
'8xl': ['6rem', { lineHeight: '1' }],
'9xl': ['8rem', { lineHeight: '1' }],
},
typography: (theme) => ({
invert: {
css: {
'--tw-prose-body': 'var(--tw-prose-invert-body)',
'--tw-prose-headings': 'var(--tw-prose-invert-headings)',
'--tw-prose-links': 'var(--tw-prose-invert-links)',
'--tw-prose-links-hover': 'var(--tw-prose-invert-links-hover)',
'--tw-prose-underline': 'var(--tw-prose-invert-underline)',
'--tw-prose-underline-hover':
'var(--tw-prose-invert-underline-hover)',
'--tw-prose-bold': 'var(--tw-prose-invert-bold)',
'--tw-prose-counters': 'var(--tw-prose-invert-counters)',
'--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
'--tw-prose-hr': 'var(--tw-prose-invert-hr)',
'--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
'--tw-prose-captions': 'var(--tw-prose-invert-captions)',
'--tw-prose-code': 'var(--tw-prose-invert-code)',
'--tw-prose-code-bg': 'var(--tw-prose-invert-code-bg)',
'--tw-prose-pre-code': 'var(--tw-prose-invert-pre-code)',
'--tw-prose-pre-bg': 'var(--tw-prose-invert-pre-bg)',
'--tw-prose-pre-border': 'var(--tw-prose-invert-pre-border)',
'--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
'--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
},
},
DEFAULT: {
css: {
'--tw-prose-body': theme('colors.zinc.600'),
'--tw-prose-headings': theme('colors.zinc.900'),
'--tw-prose-links': theme('colors.teal.500'),
'--tw-prose-links-hover': theme('colors.teal.600'),
'--tw-prose-underline': theme('colors.teal.500 / 0.2'),
'--tw-prose-underline-hover': theme('colors.teal.500'),
'--tw-prose-bold': theme('colors.zinc.900'),
'--tw-prose-counters': theme('colors.zinc.900'),
'--tw-prose-bullets': theme('colors.zinc.900'),
'--tw-prose-hr': theme('colors.zinc.100'),
'--tw-prose-quote-borders': theme('colors.zinc.200'),
'--tw-prose-captions': theme('colors.zinc.400'),
'--tw-prose-code': theme('colors.zinc.700'),
'--tw-prose-code-bg': theme('colors.zinc.300 / 0.2'),
'--tw-prose-pre-code': theme('colors.zinc.100'),
'--tw-prose-pre-bg': theme('colors.zinc.900'),
'--tw-prose-pre-border': 'transparent',
'--tw-prose-th-borders': theme('colors.zinc.200'),
'--tw-prose-td-borders': theme('colors.zinc.100'),
'--tw-prose-invert-body': theme('colors.zinc.400'),
'--tw-prose-invert-headings': theme('colors.zinc.200'),
'--tw-prose-invert-links': theme('colors.teal.400'),
'--tw-prose-invert-links-hover': theme('colors.teal.400'),
'--tw-prose-invert-underline': theme('colors.teal.400 / 0.3'),
'--tw-prose-invert-underline-hover': theme('colors.teal.400'),
'--tw-prose-invert-bold': theme('colors.zinc.200'),
'--tw-prose-invert-counters': theme('colors.zinc.200'),
'--tw-prose-invert-bullets': theme('colors.zinc.200'),
'--tw-prose-invert-hr': theme('colors.zinc.700 / 0.4'),
'--tw-prose-invert-quote-borders': theme('colors.zinc.500'),
'--tw-prose-invert-captions': theme('colors.zinc.500'),
'--tw-prose-invert-code': theme('colors.zinc.300'),
'--tw-prose-invert-code-bg': theme('colors.zinc.200 / 0.05'),
'--tw-prose-invert-pre-code': theme('colors.zinc.100'),
'--tw-prose-invert-pre-bg': 'rgb(0 0 0 / 0.4)',
'--tw-prose-invert-pre-border': theme('colors.zinc.200 / 0.1'),
'--tw-prose-invert-th-borders': theme('colors.zinc.700'),
'--tw-prose-invert-td-borders': theme('colors.zinc.800'),
// Base
color: 'var(--tw-prose-body)',
lineHeight: theme('lineHeight.7'),
'> *': {
marginTop: theme('spacing.10'),
marginBottom: theme('spacing.10'),
},
p: {
marginTop: theme('spacing.7'),
marginBottom: theme('spacing.7'),
},
// Headings
'h2, h3': {
color: 'var(--tw-prose-headings)',
fontWeight: theme('fontWeight.semibold'),
},
h2: {
fontSize: theme('fontSize.xl')[0],
lineHeight: theme('lineHeight.7'),
marginTop: theme('spacing.20'),
marginBottom: theme('spacing.4'),
},
h3: {
fontSize: theme('fontSize.base')[0],
lineHeight: theme('lineHeight.7'),
marginTop: theme('spacing.16'),
marginBottom: theme('spacing.4'),
},
':is(h2, h3) + *': {
marginTop: 0,
},
// Images
img: {
borderRadius: theme('borderRadius.3xl'),
},
// Inline elements
a: {
color: 'var(--tw-prose-links)',
fontWeight: theme('fontWeight.semibold'),
textDecoration: 'underline',
textDecorationColor: 'var(--tw-prose-underline)',
transitionProperty: 'color, text-decoration-color',
transitionDuration: theme('transitionDuration.150'),
transitionTimingFunction: theme('transitionTimingFunction.in-out'),
},
'a:hover': {
color: 'var(--tw-prose-links-hover)',
textDecorationColor: 'var(--tw-prose-underline-hover)',
},
strong: {
color: 'var(--tw-prose-bold)',
fontWeight: theme('fontWeight.semibold'),
},
code: {
display: 'inline-block',
color: 'var(--tw-prose-code)',
fontSize: theme('fontSize.sm')[0],
fontWeight: theme('fontWeight.semibold'),
backgroundColor: 'var(--tw-prose-code-bg)',
borderRadius: theme('borderRadius.lg'),
paddingLeft: theme('spacing.1'),
paddingRight: theme('spacing.1'),
},
'a code': {
color: 'inherit',
},
':is(h2, h3) code': {
fontWeight: theme('fontWeight.bold'),
},
// Quotes
blockquote: {
paddingLeft: theme('spacing.6'),
borderLeftWidth: theme('borderWidth.2'),
borderLeftColor: 'var(--tw-prose-quote-borders)',
fontStyle: 'italic',
},
// Figures
figcaption: {
color: 'var(--tw-prose-captions)',
fontSize: theme('fontSize.sm')[0],
lineHeight: theme('lineHeight.6'),
marginTop: theme('spacing.3'),
},
'figcaption > p': {
margin: 0,
},
// Lists
ul: {
listStyleType: 'disc',
},
ol: {
listStyleType: 'decimal',
},
'ul, ol': {
paddingLeft: theme('spacing.6'),
},
li: {
marginTop: theme('spacing.6'),
marginBottom: theme('spacing.6'),
paddingLeft: theme('spacing[3.5]'),
},
'li::marker': {
fontSize: theme('fontSize.sm')[0],
fontWeight: theme('fontWeight.semibold'),
},
'ol > li::marker': {
color: 'var(--tw-prose-counters)',
},
'ul > li::marker': {
color: 'var(--tw-prose-bullets)',
},
'li :is(ol, ul)': {
marginTop: theme('spacing.4'),
marginBottom: theme('spacing.4'),
},
'li :is(li, p)': {
marginTop: theme('spacing.3'),
marginBottom: theme('spacing.3'),
},
// Code blocks
pre: {
color: 'var(--tw-prose-pre-code)',
fontSize: theme('fontSize.sm')[0],
fontWeight: theme('fontWeight.medium'),
backgroundColor: 'var(--tw-prose-pre-bg)',
borderRadius: theme('borderRadius.3xl'),
padding: theme('spacing.8'),
overflowX: 'auto',
border: '1px solid',
borderColor: 'var(--tw-prose-pre-border)',
},
'pre code': {
display: 'inline',
color: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
backgroundColor: 'transparent',
borderRadius: 0,
padding: 0,
},
// Horizontal rules
hr: {
marginTop: theme('spacing.20'),
marginBottom: theme('spacing.20'),
borderTopWidth: '1px',
borderColor: 'var(--tw-prose-hr)',
'@screen lg': {
marginLeft: `calc(${theme('spacing.12')} * -1)`,
marginRight: `calc(${theme('spacing.12')} * -1)`,
},
},
// Tables
table: {
width: '100%',
tableLayout: 'auto',
textAlign: 'left',
fontSize: theme('fontSize.sm')[0],
},
thead: {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-th-borders)',
},
'thead th': {
color: 'var(--tw-prose-headings)',
fontWeight: theme('fontWeight.semibold'),
verticalAlign: 'bottom',
paddingBottom: theme('spacing.2'),
},
'thead th:not(:first-child)': {
paddingLeft: theme('spacing.2'),
},
'thead th:not(:last-child)': {
paddingRight: theme('spacing.2'),
},
'tbody tr': {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-td-borders)',
},
'tbody tr:last-child': {
borderBottomWidth: 0,
},
'tbody td': {
verticalAlign: 'baseline',
},
tfoot: {
borderTopWidth: '1px',
borderTopColor: 'var(--tw-prose-th-borders)',
},
'tfoot td': {
verticalAlign: 'top',
},
':is(tbody, tfoot) td': {
paddingTop: theme('spacing.2'),
paddingBottom: theme('spacing.2'),
},
':is(tbody, tfoot) td:not(:first-child)': {
paddingLeft: theme('spacing.2'),
},
':is(tbody, tfoot) td:not(:last-child)': {
paddingRight: theme('spacing.2'),
},
},
},
}),
},
}

28
tailwind.config.ts Normal file
View File

@ -0,0 +1,28 @@
import typographyPlugin from '@tailwindcss/typography'
import { type Config } from 'tailwindcss'
import typographyStyles from './typography'
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
darkMode: 'class',
plugins: [typographyPlugin],
theme: {
fontSize: {
xs: ['0.8125rem', { lineHeight: '1.5rem' }],
sm: ['0.875rem', { lineHeight: '1.5rem' }],
base: ['1rem', { lineHeight: '1.75rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '2rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
'4xl': ['2rem', { lineHeight: '2.5rem' }],
'5xl': ['3rem', { lineHeight: '3.5rem' }],
'6xl': ['3.75rem', { lineHeight: '1' }],
'7xl': ['4.5rem', { lineHeight: '1' }],
'8xl': ['6rem', { lineHeight: '1' }],
'9xl': ['8rem', { lineHeight: '1' }],
},
typography: typographyStyles,
},
} satisfies Config

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

283
typography.ts Normal file
View File

@ -0,0 +1,283 @@
import { type PluginUtils } from 'tailwindcss/types/config'
export default function typographyStyles({ theme }: PluginUtils) {
return {
invert: {
css: {
'--tw-prose-body': 'var(--tw-prose-invert-body)',
'--tw-prose-headings': 'var(--tw-prose-invert-headings)',
'--tw-prose-links': 'var(--tw-prose-invert-links)',
'--tw-prose-links-hover': 'var(--tw-prose-invert-links-hover)',
'--tw-prose-underline': 'var(--tw-prose-invert-underline)',
'--tw-prose-underline-hover': 'var(--tw-prose-invert-underline-hover)',
'--tw-prose-bold': 'var(--tw-prose-invert-bold)',
'--tw-prose-counters': 'var(--tw-prose-invert-counters)',
'--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
'--tw-prose-hr': 'var(--tw-prose-invert-hr)',
'--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
'--tw-prose-captions': 'var(--tw-prose-invert-captions)',
'--tw-prose-code': 'var(--tw-prose-invert-code)',
'--tw-prose-code-bg': 'var(--tw-prose-invert-code-bg)',
'--tw-prose-pre-code': 'var(--tw-prose-invert-pre-code)',
'--tw-prose-pre-bg': 'var(--tw-prose-invert-pre-bg)',
'--tw-prose-pre-border': 'var(--tw-prose-invert-pre-border)',
'--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
'--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
},
},
DEFAULT: {
css: {
'--tw-prose-body': theme('colors.zinc.600'),
'--tw-prose-headings': theme('colors.zinc.900'),
'--tw-prose-links': theme('colors.teal.500'),
'--tw-prose-links-hover': theme('colors.teal.600'),
'--tw-prose-underline': theme('colors.teal.500 / 0.2'),
'--tw-prose-underline-hover': theme('colors.teal.500'),
'--tw-prose-bold': theme('colors.zinc.900'),
'--tw-prose-counters': theme('colors.zinc.900'),
'--tw-prose-bullets': theme('colors.zinc.900'),
'--tw-prose-hr': theme('colors.zinc.100'),
'--tw-prose-quote-borders': theme('colors.zinc.200'),
'--tw-prose-captions': theme('colors.zinc.400'),
'--tw-prose-code': theme('colors.zinc.700'),
'--tw-prose-code-bg': theme('colors.zinc.300 / 0.2'),
'--tw-prose-pre-code': theme('colors.zinc.100'),
'--tw-prose-pre-bg': theme('colors.zinc.900'),
'--tw-prose-pre-border': 'transparent',
'--tw-prose-th-borders': theme('colors.zinc.200'),
'--tw-prose-td-borders': theme('colors.zinc.100'),
'--tw-prose-invert-body': theme('colors.zinc.400'),
'--tw-prose-invert-headings': theme('colors.zinc.200'),
'--tw-prose-invert-links': theme('colors.teal.400'),
'--tw-prose-invert-links-hover': theme('colors.teal.400'),
'--tw-prose-invert-underline': theme('colors.teal.400 / 0.3'),
'--tw-prose-invert-underline-hover': theme('colors.teal.400'),
'--tw-prose-invert-bold': theme('colors.zinc.200'),
'--tw-prose-invert-counters': theme('colors.zinc.200'),
'--tw-prose-invert-bullets': theme('colors.zinc.200'),
'--tw-prose-invert-hr': theme('colors.zinc.700 / 0.4'),
'--tw-prose-invert-quote-borders': theme('colors.zinc.500'),
'--tw-prose-invert-captions': theme('colors.zinc.500'),
'--tw-prose-invert-code': theme('colors.zinc.300'),
'--tw-prose-invert-code-bg': theme('colors.zinc.200 / 0.05'),
'--tw-prose-invert-pre-code': theme('colors.zinc.100'),
'--tw-prose-invert-pre-bg': 'rgb(0 0 0 / 0.4)',
'--tw-prose-invert-pre-border': theme('colors.zinc.200 / 0.1'),
'--tw-prose-invert-th-borders': theme('colors.zinc.700'),
'--tw-prose-invert-td-borders': theme('colors.zinc.800'),
// Base
color: 'var(--tw-prose-body)',
lineHeight: theme('lineHeight.7'),
'> *': {
marginTop: theme('spacing.10'),
marginBottom: theme('spacing.10'),
},
p: {
marginTop: theme('spacing.7'),
marginBottom: theme('spacing.7'),
},
// Headings
'h2, h3': {
color: 'var(--tw-prose-headings)',
fontWeight: theme('fontWeight.semibold'),
},
h2: {
fontSize: theme('fontSize.xl')[0],
lineHeight: theme('lineHeight.7'),
marginTop: theme('spacing.20'),
marginBottom: theme('spacing.4'),
},
h3: {
fontSize: theme('fontSize.base')[0],
lineHeight: theme('lineHeight.7'),
marginTop: theme('spacing.16'),
marginBottom: theme('spacing.4'),
},
':is(h2, h3) + *': {
marginTop: 0,
},
// Images
img: {
borderRadius: theme('borderRadius.3xl'),
},
// Inline elements
a: {
color: 'var(--tw-prose-links)',
fontWeight: theme('fontWeight.semibold'),
textDecoration: 'underline',
textDecorationColor: 'var(--tw-prose-underline)',
transitionProperty: 'color, text-decoration-color',
transitionDuration: theme('transitionDuration.150'),
transitionTimingFunction: theme('transitionTimingFunction.in-out'),
},
'a:hover': {
color: 'var(--tw-prose-links-hover)',
textDecorationColor: 'var(--tw-prose-underline-hover)',
},
strong: {
color: 'var(--tw-prose-bold)',
fontWeight: theme('fontWeight.semibold'),
},
code: {
display: 'inline-block',
color: 'var(--tw-prose-code)',
fontSize: theme('fontSize.sm')[0],
fontWeight: theme('fontWeight.semibold'),
backgroundColor: 'var(--tw-prose-code-bg)',
borderRadius: theme('borderRadius.lg'),
paddingLeft: theme('spacing.1'),
paddingRight: theme('spacing.1'),
},
'a code': {
color: 'inherit',
},
':is(h2, h3) code': {
fontWeight: theme('fontWeight.bold'),
},
// Quotes
blockquote: {
paddingLeft: theme('spacing.6'),
borderLeftWidth: theme('borderWidth.2'),
borderLeftColor: 'var(--tw-prose-quote-borders)',
fontStyle: 'italic',
},
// Figures
figcaption: {
color: 'var(--tw-prose-captions)',
fontSize: theme('fontSize.sm')[0],
lineHeight: theme('lineHeight.6'),
marginTop: theme('spacing.3'),
},
'figcaption > p': {
margin: 0,
},
// Lists
ul: {
listStyleType: 'disc',
},
ol: {
listStyleType: 'decimal',
},
'ul, ol': {
paddingLeft: theme('spacing.6'),
},
li: {
marginTop: theme('spacing.6'),
marginBottom: theme('spacing.6'),
paddingLeft: theme('spacing[3.5]'),
},
'li::marker': {
fontSize: theme('fontSize.sm')[0],
fontWeight: theme('fontWeight.semibold'),
},
'ol > li::marker': {
color: 'var(--tw-prose-counters)',
},
'ul > li::marker': {
color: 'var(--tw-prose-bullets)',
},
'li :is(ol, ul)': {
marginTop: theme('spacing.4'),
marginBottom: theme('spacing.4'),
},
'li :is(li, p)': {
marginTop: theme('spacing.3'),
marginBottom: theme('spacing.3'),
},
// Code blocks
pre: {
color: 'var(--tw-prose-pre-code)',
fontSize: theme('fontSize.sm')[0],
fontWeight: theme('fontWeight.medium'),
backgroundColor: 'var(--tw-prose-pre-bg)',
borderRadius: theme('borderRadius.3xl'),
padding: theme('spacing.8'),
overflowX: 'auto',
border: '1px solid',
borderColor: 'var(--tw-prose-pre-border)',
},
'pre code': {
display: 'inline',
color: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
backgroundColor: 'transparent',
borderRadius: 0,
padding: 0,
},
// Horizontal rules
hr: {
marginTop: theme('spacing.20'),
marginBottom: theme('spacing.20'),
borderTopWidth: '1px',
borderColor: 'var(--tw-prose-hr)',
'@screen lg': {
marginLeft: `calc(${theme('spacing.12')} * -1)`,
marginRight: `calc(${theme('spacing.12')} * -1)`,
},
},
// Tables
table: {
width: '100%',
tableLayout: 'auto',
textAlign: 'left',
fontSize: theme('fontSize.sm')[0],
},
thead: {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-th-borders)',
},
'thead th': {
color: 'var(--tw-prose-headings)',
fontWeight: theme('fontWeight.semibold'),
verticalAlign: 'bottom',
paddingBottom: theme('spacing.2'),
},
'thead th:not(:first-child)': {
paddingLeft: theme('spacing.2'),
},
'thead th:not(:last-child)': {
paddingRight: theme('spacing.2'),
},
'tbody tr': {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-td-borders)',
},
'tbody tr:last-child': {
borderBottomWidth: 0,
},
'tbody td': {
verticalAlign: 'baseline',
},
tfoot: {
borderTopWidth: '1px',
borderTopColor: 'var(--tw-prose-th-borders)',
},
'tfoot td': {
verticalAlign: 'top',
},
':is(tbody, tfoot) td': {
paddingTop: theme('spacing.2'),
paddingBottom: theme('spacing.2'),
},
':is(tbody, tfoot) td:not(:first-child)': {
paddingLeft: theme('spacing.2'),
},
':is(tbody, tfoot) td:not(:last-child)': {
paddingRight: theme('spacing.2'),
},
},
},
}
}