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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,6 @@ out/

# IDEA
.idea

# Generated at build time by lib/rss.js
public/feed.xml
15 changes: 2 additions & 13 deletions about.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
# About

I'm a PhD student at [UCLA](https://www.ucla.edu/) studying with [Gerard
Wong](https://samueli.ucla.edu/people/gerard-wong/). My research applies modern
ML methods such as deep kernel learning and transformers to studying the
properties and interactions of peptides to find sequences with optimal
properties for use cases like drug development!
I'm interested in problems where the answer space is too large to search by hand — and in building the tools that let us search it anyway. At [UCLA](https://www.ucla.edu/), I work with [Gerard Wong](https://samueli.ucla.edu/people/gerard-wong/) on applying ML methods like deep kernel learning and transformers to peptide sequence design.

I'm also a [Julia](https://julialang.org) enthusiast! Have a look at my lipid
phase [SAXS](https://en.wikipedia.org/wiki/Small-angle_X-ray_scattering)
analysis toolkit, [Himalaya.jl](https://github.com/jowch/Himalaya.jl) and my
[Wordle implementation](https://github.com/jowch/Wordle.jl).

Previously, I studied [Computer Science](https://cse.wustl.edu) and
[Biology](https://biology.wustl.edu) at [Washington University in St.
Louis](https://wustl.edu/), ultimately receiving BS and MS degrees.
Before that, I studied [Computer Science](https://cse.wustl.edu) and [Biology](https://biology.wustl.edu) at [Washington University in St. Louis](https://wustl.edu/), which is where I became convinced that the most interesting questions sit exactly at the seam between the two. I'm also a [Julia](https://julialang.org) enthusiast — it's a language that takes both performance and expressiveness seriously, which is rarer than it should be. My SAXS analysis toolkit [Himalaya.jl](https://github.com/jowch/Himalaya.jl) is a good example of what I mean.
17 changes: 11 additions & 6 deletions components/nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ import { faGithub, faLinkedinIn, faTwitter } from '@fortawesome/free-brands-svg-
import { faEnvelope } from '@fortawesome/free-solid-svg-icons'

const IconLink = ({ children, icon, ...props }) => (
<a {...props} className='inline-block relative p-1 m-[-4px] hover:border-b-2 hover:text-sky-900 dark:hover:text-[#84c9f2] dark:border-[#84c9f2]'>
<a {...props} className='inline-block relative p-1 m-[-4px] border-b-2 border-transparent hover:border-current hover:text-sky-900 dark:hover:text-[#84c9f2]'>
<FontAwesomeIcon icon={icon} size='sm' />
</a>
)

export default function Nav() {
return (
<nav className='mb-4 border-b-2 dark:border-[#313d43] p-3'>
<div className='max-w-screen-md mx-auto'>
<Link href="/">
<a className='text-xl hover:border-b-2 hover:text-sky-900 dark:hover:text-[#84c9f2] dark:border-[#84c9f2]'>Jonathan Chen</a>
</Link>
<div className='float-right text-lg space-x-2'>
<div className='max-w-(--breakpoint-md) mx-auto flex items-center justify-between'>
<div className='flex items-baseline gap-5'>
<Link href="/" className='text-xl border-b-2 border-transparent hover:border-current hover:text-sky-900 dark:hover:text-[#84c9f2]'>
Jonathan Chen
</Link>
<Link href="/writing" className='font-sans text-sm text-slate-500 dark:text-slate-400 hover:text-sky-900 dark:hover:text-[#84c9f2]'>
Writing
</Link>
</div>
<div className='flex items-center gap-2 text-lg'>
<IconLink href='mailto:jwhc@ucla.edu' title='Email' icon={faEnvelope} />
<IconLink href='https://github.com/jowch' title='GitHub' icon={faGithub} />
<IconLink href='https://www.linkedin.com/in/jowch/' title='LinkedIn' icon={faLinkedinIn} />
Expand Down
17 changes: 17 additions & 0 deletions components/post-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Link from 'next/link'

export default function PostCard({ post }) {
return (
<div className="border-l pl-3 border-slate-200 dark:border-slate-700 space-y-0.5">
<Link href={`/writing/${post.slug}`} className="font-sans font-medium text-base hover:underline hover:decoration-2 text-sky-900 dark:text-[#84c9f2]">
{post.title}
</Link>
{post.description && (
<p className="font-serif text-sm text-slate-600 dark:text-slate-400">{post.description}</p>
)}
<p className="font-sans text-xs text-slate-400 dark:text-slate-500">
{new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</p>
</div>
)
}
4 changes: 2 additions & 2 deletions components/pub.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ export default function Pub({ pub, ...props }) {
const { authors, title, publisher, published: year, URL } = pub

return (
<div {...props} className="max-w-prose space-y-1">
<div {...props} className="max-w-prose space-y-1 border-l pl-3 border-slate-200 dark:border-slate-700">
<h2 className="text-base font-medium">{title}</h2>
<div>
{authors.map(({ name }, i) => (
<span
key={`${props.key}-author-${i}`}
className={ME.has(name) && "font-semibold"}
className={ME.has(name) ? "font-semibold" : undefined}
>
{name}{i != authors.length - 1 && ", "}
</span>
Expand Down
58 changes: 58 additions & 0 deletions lib/posts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { promises as fs } from 'fs'
import path from 'path'
import matter from 'gray-matter'

const POSTS_DIR = path.join(process.cwd(), 'posts')

// Category display order for /writing
export const CATEGORIES = ['essay', 'note', 'curiosity']

export const CATEGORY_LABELS = {
essay: 'Essays',
note: 'Notes',
curiosity: 'Curiosities & Observations',
}

const parsePost = (filename, raw) => {
const { data, content } = matter(raw)
const slug = filename.replace(/\.md$/, '')
return { slug, content, ...data }
}

export const getAllPosts = async () => {
let files
try {
files = await fs.readdir(POSTS_DIR)
} catch (e) {
if (e.code === 'ENOENT') return []
throw e
}
const posts = await Promise.all(
files
.filter(f => f.endsWith('.md'))
.map(async filename => {
const raw = await fs.readFile(path.join(POSTS_DIR, filename), 'utf8')
return parsePost(filename, raw)
})
)
return posts.sort((a, b) => new Date(b.date) - new Date(a.date))
}

export const getPostBySlug = async slug => {
try {
const raw = await fs.readFile(path.join(POSTS_DIR, `${slug}.md`), 'utf8')
return parsePost(`${slug}.md`, raw)
} catch (e) {
if (e.code === 'ENOENT') return null
throw e
}
}

export const getPostsByCategory = async () => {
const posts = await getAllPosts()
const grouped = {}
for (const cat of CATEGORIES) {
grouped[cat] = posts.filter(p => p.category === cat)
}
return grouped
}
6 changes: 4 additions & 2 deletions lib/pubs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { parse, stringify } from 'yaml'

const CROSSREF_API = "https://api.crossref.org"

// p-throttle v8 API: pThrottle({limit, interval}) returns a factory; factory(fn) returns throttled fn.
// limit:25/interval:1000 = 25 req/sec — sufficient for CrossRef's polite pool (50 req/sec).
const throttle = pThrottle({
limit: 25,
interval: 1000
Expand All @@ -12,8 +14,8 @@ const throttle = pThrottle({
const fetchRef = throttle(doi =>
fetch(`${CROSSREF_API}/works/${doi}`)
.then(res => res.json())
.then(data => data.message),
200, {leading: true, accumulate: true})
.then(data => data.message)
)

const extractAuthors = ({ ORCID = '', given, family, sequence }) => {
return {
Expand Down
51 changes: 51 additions & 0 deletions lib/rss.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// CJS script — runs standalone via `node lib/rss.js` before/during build.
// Cannot use ESM imports since it runs outside Next.js compilation.
const fs = require('fs')
const path = require('path')
const matter = require('gray-matter')

const POSTS_DIR = path.join(process.cwd(), 'posts')
const OUT = path.join(process.cwd(), 'public', 'feed.xml')
const BASE_URL = 'https://jowch.github.io'

const getAllPosts = () => {
if (!fs.existsSync(POSTS_DIR)) return []
return fs.readdirSync(POSTS_DIR)
.filter(f => f.endsWith('.md'))
.map(filename => {
const raw = fs.readFileSync(path.join(POSTS_DIR, filename), 'utf8')
const { data, content } = matter(raw)
return { slug: filename.replace(/\.md$/, ''), content, ...data }
})
.sort((a, b) => new Date(b.date) - new Date(a.date))
}

const escapeXml = str =>
String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')

const buildFeed = posts => `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Jonathan Chen</title>
<link>${BASE_URL}</link>
<description>Writing by Jonathan Chen — essays, notes, and curiosities.</description>
<language>en-us</language>
<atom:link href="${BASE_URL}/feed.xml" rel="self" type="application/rss+xml"/>
${posts.map(post => `<item>
<title>${escapeXml(post.title)}</title>
<link>${BASE_URL}/writing/${escapeXml(post.slug)}</link>
<guid>${BASE_URL}/writing/${escapeXml(post.slug)}</guid>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
<description>${escapeXml(post.description || '')}</description>
</item>`).join('\n ')}
</channel>
</rss>`

const posts = getAllPosts()
fs.writeFileSync(OUT, buildFeed(posts), 'utf8')
console.log(`RSS feed written with ${posts.length} post(s) → ${OUT}`)
16 changes: 12 additions & 4 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
images: {
unoptimized: true,
},
output: 'export',
images: {
unoptimized: true,
},
// Pages Router doesn't fully tree-shake server-only imports (fs, path) from client
// bundles when they appear in the same file as getStaticProps. This polyfill is necessary.
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = { fs: false, path: false }
}
return config
},
}
35 changes: 18 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,27 @@
"author": "Jonathan Chen <jwhc@ucla.edu>",
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start"
"dev": "next dev --webpack",
"prebuild": "node lib/rss.js",
"build": "next build --webpack",
"start": "npx serve out"
},
"dependencies": {
"next": "^12.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"gray-matter": "^4.0.3",
"next": "^16.2.4",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@heroicons/react": "^1.0.6",
"autoprefixer": "^10.4.7",
"markdown-to-jsx": "^7.1.7",
"p-throttle": "^5.0.0",
"postcss": "^8.4.14",
"tailwindcss": "^3.1.6",
"yaml": "^2.1.1"
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.3.1",
"@tailwindcss/postcss": "^4.2.4",
"markdown-to-jsx": "^9.7.16",
"p-throttle": "^8.1.0",
"postcss": "^8.5.13",
"tailwindcss": "^4.2.4",
"yaml": "^2.8.4"
}
}
Loading