Update website to use typescript
This commit is contained in:
parent
21a0a1f2d7
commit
8f9757936e
@ -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
1
.env.example
Normal file
@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_SITE_URL=https://example.com
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
7
mdx-components.tsx
Normal file
7
mdx-components.tsx
Normal 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
5
next-env.d.ts
vendored
Normal 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.
|
@ -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
763
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'postcss-focus-visible': {
|
||||
replaceWith: '[data-focus-visible-added]',
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
@ -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
161
src/app/about/page.tsx
Normal 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:
|
||||
'I’m 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">
|
||||
I’m 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>
|
||||
I’m 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 I’m 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. I’m
|
||||
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>
|
||||
)
|
||||
}
|
@ -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/).
|
@ -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
60
src/app/articles/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
62
src/app/feed.xml/route.ts
Normal file
62
src/app/feed.xml/route.ts
Normal 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
38
src/app/layout.tsx
Normal 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:
|
||||
'I’m 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
23
src/app/not-found.tsx
Normal 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 couldn’t find the page you’re looking for.
|
||||
</p>
|
||||
<Button href="/" variant="secondary" className="mt-4">
|
||||
Go back home
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -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="I’m 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
93
src/app/projects/page.tsx
Normal 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
30
src/app/providers.tsx
Normal 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
98
src/app/speaking/page.tsx
Normal 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:
|
||||
'I’ve spoken at events all around the world and been interviewed for many podcasts.',
|
||||
}
|
||||
|
||||
export default function Speaking() {
|
||||
return (
|
||||
<SimpleLayout
|
||||
title="I’ve 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 there’s 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 you’re not embarassed by your first version, you’re doing it wrong. Well when you’re selling DIY space shuttle kits it turns out it’s 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
123
src/app/uses/page.tsx
Normal 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 I’m being productive when I’m really just procrastinating. Here’s 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. I’ve 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 you’re working at planetary scale, every pixel
|
||||
you can get counts.
|
||||
</Tool>
|
||||
<Tool title="IBM Model M SSK Industrial Keyboard">
|
||||
They don’t 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 I’m 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 don’t care if it’s 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">
|
||||
I’m honestly not even sure what features I get with this that aren’t
|
||||
just part of the macOS Terminal but it’s 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 it’s 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">
|
||||
It’s not the newest kid on the block but it’s 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, it’s 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
58
src/components/ArticleLayout.tsx
Normal file
58
src/components/ArticleLayout.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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} />
|
||||
)
|
||||
}
|
@ -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(
|
@ -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
|
39
src/components/Container.tsx
Normal file
39
src/components/Container.tsx
Normal 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>
|
||||
)
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
19
src/components/Layout.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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
10
src/components/Prose.tsx
Normal 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} />
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 (
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
36
src/lib/articles.ts
Normal 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))
|
||||
}
|
@ -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',
|
@ -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'),
|
||||
])
|
||||
}
|
@ -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))
|
||||
}
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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="I’m 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">
|
||||
I’m 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>
|
||||
I’m 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 I’m 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. I’m 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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="I’ve spoken at events all around the world and been interviewed for many podcasts."
|
||||
/>
|
||||
</Head>
|
||||
<SimpleLayout
|
||||
title="I’ve 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 there’s 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 you’re not embarassed by your first version, you’re doing it wrong. Well when you’re selling DIY space shuttle kits it turns out it’s 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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 I’m being productive when I’m really just procrastinating. Here’s 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. I’ve 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 you’re working at planetary scale, every
|
||||
pixel you can get counts.
|
||||
</Tool>
|
||||
<Tool title="IBM Model M SSK Industrial Keyboard">
|
||||
They don’t 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 I’m 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 don’t care if it’s 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">
|
||||
I’m honestly not even sure what features I get with this that
|
||||
aren’t just part of the macOS Terminal but it’s 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 it’s 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">
|
||||
It’s not the newest kid on the block but it’s 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, it’s 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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
28
tailwind.config.ts
Normal 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
28
tsconfig.json
Normal 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
283
typography.ts
Normal 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'),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user