init: add project files

This commit is contained in:
Prad Nukala
2023-09-09 22:30:53 -04:00
commit 4aee94eae5
59 changed files with 20670 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
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,
InstagramIcon,
LinkedInIcon,
TwitterIcon,
} from '@/components/SocialIcons'
import portraitImage from '@/images/portrait.jpg'
function SocialLink({
className,
href,
children,
icon: Icon,
}: {
className?: string
href: string
icon: React.ComponentType<{ className?: string }>
children: React.ReactNode
}) {
return (
<li className={clsx(className, 'flex')}>
<Link
href={href}
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
>
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
<span className="ml-4">{children}</span>
</Link>
</li>
)
}
function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
d="M6 5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6Zm.245 2.187a.75.75 0 0 0-.99 1.126l6.25 5.5a.75.75 0 0 0 .99 0l6.25-5.5a.75.75 0 0 0-.99-1.126L12 12.251 6.245 7.187Z"
/>
</svg>
)
}
export const metadata: Metadata = {
title: 'About',
description:
'Im Spencer Sharp. I live in New York City, where I design the future.',
}
export default function About() {
return (
<Container className="mt-16 sm:mt-32">
<div className="grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
<div className="lg:pl-20">
<div className="max-w-xs px-2.5 lg:max-w-none">
<Image
src={portraitImage}
alt=""
sizes="(min-width: 1024px) 32rem, 20rem"
className="aspect-square rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
/>
</div>
</div>
<div className="lg:order-first lg:row-span-2">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
Im Spencer Sharp. I live in New York City, where I design the
future.
</h1>
<div className="mt-6 space-y-7 text-base text-zinc-600 dark:text-zinc-400">
<p>
Ive loved making things for as long as I can remember, and wrote
my first program when I was 6 years old, just two weeks after my
mom brought home the brand new Macintosh LC 550 that I taught
myself to type on.
</p>
<p>
The only thing I loved more than computers as a kid was space.
When I was 8, I climbed the 40-foot oak tree at the back of our
yard while wearing my older sisters motorcycle helmet, counted
down from three, and jumped hoping the tree was tall enough that
with just a bit of momentum Id be able to get to orbit.
</p>
<p>
I spent the next few summers indoors working on a rocket design,
while I recovered from the multiple surgeries it took to fix my
badly broken legs. It took nine iterations, but when I was 15 I
sent my dads Blackberry into orbit and was able to transmit a
photo back down to our family computer from space.
</p>
<p>
Today, Im the founder of Planetaria, where were working on
civilian space suits and manned shuttle kits you can assemble at
home so that the next generation of kids really <em>can</em> make
it to orbit from the comfort of their own backyards.
</p>
</div>
</div>
<div className="lg:pl-20">
<ul role="list">
<SocialLink href="#" icon={TwitterIcon}>
Follow on Twitter
</SocialLink>
<SocialLink href="#" icon={InstagramIcon} className="mt-4">
Follow on Instagram
</SocialLink>
<SocialLink href="#" icon={GitHubIcon} className="mt-4">
Follow on GitHub
</SocialLink>
<SocialLink href="#" icon={LinkedInIcon} className="mt-4">
Follow on LinkedIn
</SocialLink>
<SocialLink
href="mailto:spencer@planetaria.tech"
icon={MailIcon}
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
>
spencer@planetaria.tech
</SocialLink>
</ul>
</div>
</div>
</Container>
)
}
@@ -0,0 +1,91 @@
import { ArticleLayout } from '@/components/ArticleLayout'
import Image from 'next/image'
import designSystem from './planetaria-design-system.png'
export const article = {
author: 'Adam Wathan',
date: '2022-09-05',
title: 'Crafting a design system for a multiplanetary future',
description:
'Most companies try to stay ahead of the curve when it comes to visual design, but for Planetaria we needed to create a brand that would still inspire us 100 years from now when humanity has spread across our entire solar system.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
Most companies try to stay ahead of the curve when it comes to visual design, but for Planetaria we needed to create a brand that would still inspire us 100 years from now when humanity has spread across our entire solar system.
<Image src={designSystem} alt="" />
I knew that to get it right I was going to have to replicate the viewing conditions of someone from the future, so I grabbed my space helmet from the closet, created a new Figma document, and got to work.
## Sermone fata
Lorem markdownum, bracchia in redibam! Terque unda puppi nec, linguae posterior
in utraque respicere candidus Mimasque formae; quae conantem cervice. Parcite
variatus, redolentia adeunt. Tyrioque dies, naufraga sua adit partibus celanda
torquere temptata, erit maneat et ramos, [iam](#) ait dominari
potitus! Tibi litora matremque fumantia condi radicibus opusque.
Deus feram verumque, fecit, ira tamen, terras per alienae victum. Mutantur
levitate quas ubi arcum ripas oculos abest. Adest [commissaque
victae](#) in gemitus nectareis ire diva
dotibus ora, et findi huic invenit; fatis? Fractaque dare superinposita
nimiumque simulatoremque sanguine, at voce aestibus diu! Quid veterum hausit tu
nil utinam paternos ima, commentaque.
```c
exbibyte_wins = gigahertz(3);
grayscaleUtilityClient = control_uat;
pcmciaHibernate = oop_virus_console(text_mountain);
if (stateWaisFirewire >= -2) {
jfs = 647065 / ldapVrml(tutorialRestore, 85);
metal_runtime_parse = roomComputingResolution - toolbarUpload +
ipx_nvram_open;
} else {
maximizeSidebar *= suffix_url(flatbed + 2, requirements_encoding_node +
only_qbe_media, minicomputer);
}
```
Aere repetiti cognataque natus. Habebat vela solutis saepe munus nondum adhuc
oscula nomina pignora corpus deserat.
## Lethaei Pindumve me quae dinumerat Pavor
Idem se saxa fata pollentibus geminos; quos pedibus. Est urnis Herses omnes nec
divite: et ille illa furit sim verbis Cyllenius.
1. Captus inpleverunt collo
2. Nec nam placebant
3. Siquos vulgus
4. Dictis carissime fugae
5. A tacitos nulla viginti
Ungues fistula annoso, ille addit linoque motatque uberior verso
[rubuerunt](#) confine desuetaque. _Sanguine_ anteit
emerguntque expugnacior est pennas iniqui ecce **haeret** genus: peiora imagine
fossas Cephisos formosa! Refugitque amata [refelli](#)
supplex. Summa brevis vetuere tenebas, hostes vetantis, suppressit, arreptum
regna. Postquam conpescit iuvenis habet corpus, et erratica, perdere, tot mota
ars talis.
```c
digital.webcam_dual_frequency = webmasterMms;
if (5 + language_standalone_google) {
cc_inbox_layout *= file_character;
task += p;
lockUnicode += enterprise_monochrome(tokenFunctionPersonal, keyVirtual,
adf);
}
windows_binary_esports(87734, array(restoreRomTopology, adRaw(407314),
dongleBashThumbnail), interpreter);
```
Sit volat naturam; motu Cancri. Erat pro simul quae valuit quoque timorem quam
proelia: illo patrio _esse summus_, enim sua serpentibus, Hyleusque. Est coniuge
recuso; refert Coroniden ignotos manat, adfectu.
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

@@ -0,0 +1,106 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const article = {
author: 'Adam Wathan',
date: '2022-09-02',
title: 'Introducing Animaginary: High performance web animations',
description:
'When youre building a website for a company as ambitious as Planetaria, you need to make an impression. I wanted people to visit our website and see animations that looked more realistic than reality itself.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
When youre building a website for a company as ambitious as Planetaria, you need to make an impression. I wanted people to visit our website and see animations that looked more realistic than reality itself.
To make this possible, we needed to squeeze every drop of performance out of the browser possible. And so Animaginary was born.
```js
import { animate } from '@planetaria/animaginary'
export function MyComponent({ open, children }) {
return (
<animate.div
in={open}
animateFrom="opacity-0 scale-95"
animateTo="opacity-100 scale-100"
duration={350}
>
{children}
</animate.div>
)
}
```
Animaginary is our new web animation library that redefines what you thought was possible on the web. Hand-written in optimized WASM, Animaginary can even animate the `height` property of an element at 60fps.
## Sermone fata
Lorem markdownum, bracchia in redibam! Terque unda puppi nec, linguae posterior
in utraque respicere candidus Mimasque formae; quae conantem cervice. Parcite
variatus, redolentia adeunt. Tyrioque dies, naufraga sua adit partibus celanda
torquere temptata, erit maneat et ramos, [iam](#) ait dominari
potitus! Tibi litora matremque fumantia condi radicibus opusque.
Deus feram verumque, fecit, ira tamen, terras per alienae victum. Mutantur
levitate quas ubi arcum ripas oculos abest. Adest [commissaque
victae](#) in gemitus nectareis ire diva
dotibus ora, et findi huic invenit; fatis? Fractaque dare superinposita
nimiumque simulatoremque sanguine, at voce aestibus diu! Quid veterum hausit tu
nil utinam paternos ima, commentaque.
```c
exbibyte_wins = gigahertz(3);
grayscaleUtilityClient = control_uat;
pcmciaHibernate = oop_virus_console(text_mountain);
if (stateWaisFirewire >= -2) {
jfs = 647065 / ldapVrml(tutorialRestore, 85);
metal_runtime_parse = roomComputingResolution - toolbarUpload +
ipx_nvram_open;
} else {
maximizeSidebar *= suffix_url(flatbed + 2, requirements_encoding_node +
only_qbe_media, minicomputer);
}
```
Aere repetiti cognataque natus. Habebat vela solutis saepe munus nondum adhuc
oscula nomina pignora corpus deserat.
## Lethaei Pindumve me quae dinumerat Pavor
Idem se saxa fata pollentibus geminos; quos pedibus. Est urnis Herses omnes nec
divite: et ille illa furit sim verbis Cyllenius.
1. Captus inpleverunt collo
2. Nec nam placebant
3. Siquos vulgus
4. Dictis carissime fugae
5. A tacitos nulla viginti
Ungues fistula annoso, ille addit linoque motatque uberior verso
[rubuerunt](#) confine desuetaque. _Sanguine_ anteit
emerguntque expugnacior est pennas iniqui ecce **haeret** genus: peiora imagine
fossas Cephisos formosa! Refugitque amata [refelli](#)
supplex. Summa brevis vetuere tenebas, hostes vetantis, suppressit, arreptum
regna. Postquam conpescit iuvenis habet corpus, et erratica, perdere, tot mota
ars talis.
```c
digital.webcam_dual_frequency = webmasterMms;
if (5 + language_standalone_google) {
cc_inbox_layout *= file_character;
task += p;
lockUnicode += enterprise_monochrome(tokenFunctionPersonal, keyVirtual,
adf);
}
windows_binary_esports(87734, array(restoreRomTopology, adRaw(407314),
dongleBashThumbnail), interpreter);
```
Sit volat naturam; motu Cancri. Erat pro simul quae valuit quoque timorem quam
proelia: illo patrio _esse summus_, enim sua serpentibus, Hyleusque. Est coniuge
recuso; refert Coroniden ignotos manat, adfectu.
+60
View File
@@ -0,0 +1,60 @@
import { type Metadata } from 'next'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import { type ArticleWithSlug, getAllArticles } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
function Article({ article }: { article: ArticleWithSlug }) {
return (
<article className="md:grid md:grid-cols-4 md:items-baseline">
<Card className="md:col-span-3">
<Card.Title href={`/articles/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="md:hidden"
decorate
>
{formatDate(article.date)}
</Card.Eyebrow>
<Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta>
</Card>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="mt-1 hidden md:block"
>
{formatDate(article.date)}
</Card.Eyebrow>
</article>
)
}
export const metadata: Metadata = {
title: 'Articles',
description:
'All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order.',
}
export default async function ArticlesIndex() {
let articles = await getAllArticles()
return (
<SimpleLayout
title="Writing on software design, company building, and the aerospace industry."
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>
)
}
@@ -0,0 +1,101 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const article = {
author: 'Adam Wathan',
date: '2022-07-14',
title: 'Rewriting the cosmOS kernel in Rust',
description:
'When we released the first version of cosmOS last year, it was written in Go. Go is a wonderful programming language, but its been a while since Ive seen an article on the front page of Hacker News about rewriting some important tool in Go and I see articles on there about rewriting things in Rust every single week.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
When we released the first version of cosmOS last year, it was written in Go. Go is a wonderful programming language with a lot of benefits, but its been a while since Ive seen an article on the front page of Hacker News about rewriting some important tool in Go and I see articles on there about rewriting things in Rust every single week.
```rust
use ferris_says::say;
use std::io::{stdout, BufWriter};
fn main() {
let stdout = stdout();
let message = String::from("Hello fellow hackers");
let width = message.chars().count();
let mut writer = BufWriter::new(stdout.lock());
say(message.as_bytes(), width, &mut writer).unwrap();
}
```
I derive a large amount of my self-worth from whether or not Hacker News is impressed with the work I'm doing, so when I realized this, I cancelled all of our existing projects and started migrating everything to Rust immediately.
## Sermone fata
Lorem markdownum, bracchia in redibam! Terque unda puppi nec, linguae posterior
in utraque respicere candidus Mimasque formae; quae conantem cervice. Parcite
variatus, redolentia adeunt. Tyrioque dies, naufraga sua adit partibus celanda
torquere temptata, erit maneat et ramos, [iam](#) ait dominari
potitus! Tibi litora matremque fumantia condi radicibus opusque.
Deus feram verumque, fecit, ira tamen, terras per alienae victum. Mutantur
levitate quas ubi arcum ripas oculos abest. Adest [commissaque
victae](#) in gemitus nectareis ire diva
dotibus ora, et findi huic invenit; fatis? Fractaque dare superinposita
nimiumque simulatoremque sanguine, at voce aestibus diu! Quid veterum hausit tu
nil utinam paternos ima, commentaque.
```c
exbibyte_wins = gigahertz(3);
grayscaleUtilityClient = control_uat;
pcmciaHibernate = oop_virus_console(text_mountain);
if (stateWaisFirewire >= -2) {
jfs = 647065 / ldapVrml(tutorialRestore, 85);
metal_runtime_parse = roomComputingResolution - toolbarUpload +
ipx_nvram_open;
} else {
maximizeSidebar *= suffix_url(flatbed + 2, requirements_encoding_node +
only_qbe_media, minicomputer);
}
```
Aere repetiti cognataque natus. Habebat vela solutis saepe munus nondum adhuc
oscula nomina pignora corpus deserat.
## Lethaei Pindumve me quae dinumerat Pavor
Idem se saxa fata pollentibus geminos; quos pedibus. Est urnis Herses omnes nec
divite: et ille illa furit sim verbis Cyllenius.
1. Captus inpleverunt collo
2. Nec nam placebant
3. Siquos vulgus
4. Dictis carissime fugae
5. A tacitos nulla viginti
Ungues fistula annoso, ille addit linoque motatque uberior verso
[rubuerunt](#) confine desuetaque. _Sanguine_ anteit
emerguntque expugnacior est pennas iniqui ecce **haeret** genus: peiora imagine
fossas Cephisos formosa! Refugitque amata [refelli](#)
supplex. Summa brevis vetuere tenebas, hostes vetantis, suppressit, arreptum
regna. Postquam conpescit iuvenis habet corpus, et erratica, perdere, tot mota
ars talis.
```c
digital.webcam_dual_frequency = webmasterMms;
if (5 + language_standalone_google) {
cc_inbox_layout *= file_character;
task += p;
lockUnicode += enterprise_monochrome(tokenFunctionPersonal, keyVirtual,
adf);
}
windows_binary_esports(87734, array(restoreRomTopology, adRaw(407314),
dongleBashThumbnail), interpreter);
```
Sit volat naturam; motu Cancri. Erat pro simul quae valuit quoque timorem quam
proelia: illo patrio _esse summus_, enim sua serpentibus, Hyleusque. Est coniuge
recuso; refert Coroniden ignotos manat, adfectu.
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+70
View File
@@ -0,0 +1,70 @@
import assert from 'assert'
import * as cheerio from 'cheerio'
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: 'Spencer Sharp',
email: 'spencer@planetaria.tech',
}
let feed = new Feed({
title: author.name,
description: 'Your blog description',
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 articleIds = require
.context('../articles', true, /\/page\.mdx$/)
.keys()
.filter((key) => key.startsWith('./'))
.map((key) => key.slice(2).replace(/\/page\.mdx$/, ''))
for (let id of articleIds) {
let url = String(new URL(`/articles/${id}`, req.url))
let html = await (await fetch(url)).text()
let $ = cheerio.load(html)
let publicUrl = `${siteUrl}/articles/${id}`
let article = $('article').first()
let title = article.find('h1').first().text()
let date = article.find('time').first().attr('datetime')
let content = article.find('[data-mdx-content]').first().html()
assert(typeof title === 'string')
assert(typeof date === 'string')
assert(typeof content === 'string')
feed.addItem({
title,
id: publicUrl,
link: publicUrl,
content,
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',
},
})
}
+39
View File
@@ -0,0 +1,39 @@
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 - Spencer Sharp',
default:
'Spencer Sharp - Software designer, founder, and amateur astronaut',
},
description:
'Im Spencer, a software designer and entrepreneur based in New York City. Im the founder and CEO of Planetaria, where we develop technologies that empower regular people to explore space on their own terms.',
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
View File
@@ -0,0 +1,23 @@
import { Button } from '@/components/Button'
import { Container } from '@/components/Container'
export default function NotFound() {
return (
<Container className="flex h-full items-center pt-16 sm:pt-32">
<div className="flex flex-col items-center">
<p className="text-base font-semibold text-zinc-400 dark:text-zinc-500">
404
</p>
<h1 className="mt-4 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
Page not found
</h1>
<p className="mt-4 text-base text-zinc-600 dark:text-zinc-400">
Sorry, we couldnt find the page youre looking for.
</p>
<Button href="/" variant="secondary" className="mt-4">
Go back home
</Button>
</div>
</Container>
)
}
+324
View File
@@ -0,0 +1,324 @@
import Image, { type ImageProps } from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { Button } from '@/components/Button'
import { Card } from '@/components/Card'
import { Container } from '@/components/Container'
import {
GitHubIcon,
InstagramIcon,
LinkedInIcon,
TwitterIcon,
} from '@/components/SocialIcons'
import logoAirbnb from '@/images/logos/airbnb.svg'
import logoFacebook from '@/images/logos/facebook.svg'
import logoPlanetaria from '@/images/logos/planetaria.svg'
import logoStarbucks from '@/images/logos/starbucks.svg'
import image1 from '@/images/photos/image-1.jpg'
import image2 from '@/images/photos/image-2.jpg'
import image3 from '@/images/photos/image-3.jpg'
import image4 from '@/images/photos/image-4.jpg'
import image5 from '@/images/photos/image-5.jpg'
import { type ArticleWithSlug, getAllArticles } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path
d="M2.75 7.75a3 3 0 0 1 3-3h12.5a3 3 0 0 1 3 3v8.5a3 3 0 0 1-3 3H5.75a3 3 0 0 1-3-3v-8.5Z"
className="fill-zinc-100 stroke-zinc-400 dark:fill-zinc-100/10 dark:stroke-zinc-500"
/>
<path
d="m4 6 6.024 5.479a2.915 2.915 0 0 0 3.952 0L20 6"
className="stroke-zinc-400 dark:stroke-zinc-500"
/>
</svg>
)
}
function BriefcaseIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path
d="M2.75 9.75a3 3 0 0 1 3-3h12.5a3 3 0 0 1 3 3v8.5a3 3 0 0 1-3 3H5.75a3 3 0 0 1-3-3v-8.5Z"
className="fill-zinc-100 stroke-zinc-400 dark:fill-zinc-100/10 dark:stroke-zinc-500"
/>
<path
d="M3 14.25h6.249c.484 0 .952-.002 1.316.319l.777.682a.996.996 0 0 0 1.316 0l.777-.682c.364-.32.832-.319 1.316-.319H21M8.75 6.5V4.75a2 2 0 0 1 2-2h2.5a2 2 0 0 1 2 2V6.5"
className="stroke-zinc-400 dark:stroke-zinc-500"
/>
</svg>
)
}
function ArrowDownIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
d="M4.75 8.75 8 12.25m0 0 3.25-3.5M8 12.25v-8.5"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function Article({ article }: { article: ArticleWithSlug }) {
return (
<Card as="article">
<Card.Title href={`/articles/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow as="time" dateTime={article.date} decorate>
{formatDate(article.date)}
</Card.Eyebrow>
<Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta>
</Card>
)
}
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" />
</Link>
)
}
function Newsletter() {
return (
<form
action="/thank-you"
className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40"
>
<h2 className="flex text-sm font-semibold text-zinc-900 dark:text-zinc-100">
<MailIcon className="h-6 w-6 flex-none" />
<span className="ml-3">Stay up to date</span>
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
Get notified when I publish something new, and unsubscribe at any time.
</p>
<div className="mt-6 flex">
<input
type="email"
placeholder="Email address"
aria-label="Email address"
required
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
/>
<Button type="submit" className="ml-4 flex-none">
Join
</Button>
</div>
</form>
)
}
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-full 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: Array<Role> = [
{
company: 'Planetaria',
title: 'CEO',
logo: logoPlanetaria,
start: '2019',
end: {
label: 'Present',
dateTime: new Date().getFullYear().toString(),
},
},
{
company: 'Airbnb',
title: 'Product Designer',
logo: logoAirbnb,
start: '2014',
end: '2019',
},
{
company: 'Facebook',
title: 'iOS Software Engineer',
logo: logoFacebook,
start: '2011',
end: '2014',
},
{
company: 'Starbucks',
title: 'Shift Supervisor',
logo: logoStarbucks,
start: '2008',
end: '2011',
},
]
return (
<div className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40">
<h2 className="flex text-sm font-semibold text-zinc-900 dark:text-zinc-100">
<BriefcaseIcon className="h-6 w-6 flex-none" />
<span className="ml-3">Work</span>
</h2>
<ol className="mt-6 space-y-4">
{resume.map((role, roleIndex) => (
<Role key={roleIndex} role={role} />
))}
</ol>
<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>
</div>
)
}
function Photos() {
let rotations = ['rotate-2', '-rotate-2', 'rotate-2', 'rotate-2', '-rotate-2']
return (
<div className="mt-16 sm:mt-20">
<div className="-my-4 flex justify-center gap-5 overflow-hidden py-4 sm:gap-8">
{[image1, image2, image3, image4, image5].map((image, imageIndex) => (
<div
key={image.src}
className={clsx(
'relative aspect-[9/10] w-44 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800 sm:w-72 sm:rounded-2xl',
rotations[imageIndex % rotations.length],
)}
>
<Image
src={image}
alt=""
sizes="(min-width: 640px) 18rem, 11rem"
className="absolute inset-0 h-full w-full object-cover"
/>
</div>
))}
</div>
</div>
)
}
export default async function Home() {
let articles = (await getAllArticles()).slice(0, 4)
return (
<>
<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">
Software designer, founder, and amateur astronaut.
</h1>
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
Im Spencer, a software designer and entrepreneur based in New York
City. Im the founder and CEO of Planetaria, where we develop
technologies that empower regular people to explore space on their
own terms.
</p>
<div className="mt-6 flex gap-6">
<SocialLink
href="https://twitter.com"
aria-label="Follow on Twitter"
icon={TwitterIcon}
/>
<SocialLink
href="https://instagram.com"
aria-label="Follow on Instagram"
icon={InstagramIcon}
/>
<SocialLink
href="https://github.com"
aria-label="Follow on GitHub"
icon={GitHubIcon}
/>
<SocialLink
href="https://linkedin.com"
aria-label="Follow on LinkedIn"
icon={LinkedInIcon}
/>
</div>
</div>
</Container>
<Photos />
<Container className="mt-24 md:mt-28">
<div className="mx-auto grid max-w-xl grid-cols-1 gap-y-20 lg:max-w-none lg:grid-cols-2">
<div className="flex flex-col gap-16">
{articles.map((article) => (
<Article key={article.slug} article={article} />
))}
</div>
<div className="space-y-10 lg:pl-16 xl:pl-24">
<Newsletter />
<Resume />
</div>
</div>
</Container>
</>
)
}
+99
View File
@@ -0,0 +1,99 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import logoAnimaginary from '@/images/logos/animaginary.svg'
import logoCosmos from '@/images/logos/cosmos.svg'
import logoHelioStream from '@/images/logos/helio-stream.svg'
import logoOpenShuttle from '@/images/logos/open-shuttle.svg'
import logoPlanetaria from '@/images/logos/planetaria.svg'
const projects = [
{
name: 'Planetaria',
description:
'Creating technology to empower civilians to explore space on their own terms.',
link: { href: 'http://planetaria.tech', label: 'planetaria.tech' },
logo: logoPlanetaria,
},
{
name: 'Animaginary',
description:
'High performance web animation library, hand-written in optimized WASM.',
link: { href: '#', label: 'github.com' },
logo: logoAnimaginary,
},
{
name: 'HelioStream',
description:
'Real-time video streaming library, optimized for interstellar transmission.',
link: { href: '#', label: 'github.com' },
logo: logoHelioStream,
},
{
name: 'cosmOS',
description:
'The operating system that powers our Planetaria space shuttles.',
link: { href: '#', label: 'github.com' },
logo: logoCosmos,
},
{
name: 'OpenShuttle',
description:
'The schematics for the first rocket I designed that successfully made it to orbit.',
link: { href: '#', label: 'github.com' },
logo: logoOpenShuttle,
},
]
function LinkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
d="M15.712 11.823a.75.75 0 1 0 1.06 1.06l-1.06-1.06Zm-4.95 1.768a.75.75 0 0 0 1.06-1.06l-1.06 1.06Zm-2.475-1.414a.75.75 0 1 0-1.06-1.06l1.06 1.06Zm4.95-1.768a.75.75 0 1 0-1.06 1.06l1.06-1.06Zm3.359.53-.884.884 1.06 1.06.885-.883-1.061-1.06Zm-4.95-2.12 1.414-1.415L12 6.344l-1.415 1.413 1.061 1.061Zm0 3.535a2.5 2.5 0 0 1 0-3.536l-1.06-1.06a4 4 0 0 0 0 5.656l1.06-1.06Zm4.95-4.95a2.5 2.5 0 0 1 0 3.535L17.656 12a4 4 0 0 0 0-5.657l-1.06 1.06Zm1.06-1.06a4 4 0 0 0-5.656 0l1.06 1.06a2.5 2.5 0 0 1 3.536 0l1.06-1.06Zm-7.07 7.07.176.177 1.06-1.06-.176-.177-1.06 1.06Zm-3.183-.353.884-.884-1.06-1.06-.884.883 1.06 1.06Zm4.95 2.121-1.414 1.414 1.06 1.06 1.415-1.413-1.06-1.061Zm0-3.536a2.5 2.5 0 0 1 0 3.536l1.06 1.06a4 4 0 0 0 0-5.656l-1.06 1.06Zm-4.95 4.95a2.5 2.5 0 0 1 0-3.535L6.344 12a4 4 0 0 0 0 5.656l1.06-1.06Zm-1.06 1.06a4 4 0 0 0 5.657 0l-1.061-1.06a2.5 2.5 0 0 1-3.535 0l-1.061 1.06Zm7.07-7.07-.176-.177-1.06 1.06.176.178 1.06-1.061Z"
fill="currentColor"
/>
</svg>
)
}
export const metadata: Metadata = {
title: 'Projects',
description: 'Things Ive made trying to put my dent in the universe.',
}
export default function Projects() {
return (
<SimpleLayout
title="Things Ive made trying to put my dent in the universe."
intro="Ive worked on tons of little projects over the years but these are the ones that Im most proud of. Many of them are open-source, so if you see something that piques your interest, check out the code and contribute if you have ideas for how it can be improved."
>
<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-full 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>
)
}
+55
View File
@@ -0,0 +1,55 @@
'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
}
function ThemeWatcher() {
let { resolvedTheme, setTheme } = useTheme()
useEffect(() => {
let media = window.matchMedia('(prefers-color-scheme: dark)')
function onMediaChange() {
let systemTheme = media.matches ? 'dark' : 'light'
if (resolvedTheme === systemTheme) {
setTheme('system')
}
}
onMediaChange()
media.addEventListener('change', onMediaChange)
return () => {
media.removeEventListener('change', onMediaChange)
}
}, [resolvedTheme, setTheme])
return null
}
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>
<ThemeWatcher />
{children}
</ThemeProvider>
</AppContext.Provider>
)
}
+98
View File
@@ -0,0 +1,98 @@
import { type Metadata } from 'next'
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function SpeakingSection({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Section>) {
return (
<Section {...props}>
<div className="space-y-16">{children}</div>
</Section>
)
}
function Appearance({
title,
description,
event,
cta,
href,
}: {
title: string
description: string
event: string
cta: string
href: string
}) {
return (
<Card as="article">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Eyebrow decorate>{event}</Card.Eyebrow>
<Card.Description>{description}</Card.Description>
<Card.Cta>{cta}</Card.Cta>
</Card>
)
}
export const metadata: Metadata = {
title: 'Speaking',
description:
'Ive spoken at events all around the world and been interviewed for many podcasts.',
}
export default function Speaking() {
return (
<SimpleLayout
title="Ive spoken at events all around the world and been interviewed for many podcasts."
intro="One of my favorite ways to share my ideas is live on stage, where theres so much more communication bandwidth than there is in writing, and I love podcast interviews because they give me the opportunity to answer questions instead of just present my opinions."
>
<div className="space-y-20">
<SpeakingSection title="Conferences">
<Appearance
href="#"
title="In space, no one can watch you stream — until now"
description="A technical deep-dive into HelioStream, the real-time streaming library I wrote for transmitting live video back to Earth."
event="SysConf 2021"
cta="Watch video"
/>
<Appearance
href="#"
title="Lessons learned from our first product recall"
description="They say that if youre not embarassed by your first version, youre doing it wrong. Well when youre selling DIY space shuttle kits it turns out its a bit more complicated."
event="Business of Startups 2020"
cta="Watch video"
/>
</SpeakingSection>
<SpeakingSection title="Podcasts">
<Appearance
href="#"
title="Using design as a competitive advantage"
description="How we used world-class visual design to attract a great team, win over customers, and get more press for Planetaria."
event="Encoding Design, July 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Bootstrapping an aerospace company to $17M ARR"
description="The story of how we built one of the most promising space startups in the world without taking any capital from investors."
event="The Escape Velocity Show, March 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Programming your company operating system"
description="On the importance of creating systems and processes for running your business so that everyone on the team knows how to make the right decision no matter the situation."
event="How They Work Radio, September 2021"
cta="Listen to podcast"
/>
</SpeakingSection>
</div>
</SimpleLayout>
)
}
+17
View File
@@ -0,0 +1,17 @@
import { type Metadata } from 'next'
import { SimpleLayout } from '@/components/SimpleLayout'
export const metadata: Metadata = {
title: 'Youre subscribed',
description: 'Thanks for subscribing to my newsletter.',
}
export default function ThankYou() {
return (
<SimpleLayout
title="Thanks for subscribing."
intro="Ill send you an email any time I publish a new blog post, release a new project, or have anything interesting to share that I think youd want to hear about. You can unsubscribe at any time, no hard feelings."
/>
)
}
+123
View File
@@ -0,0 +1,123 @@
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function ToolsSection({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Section>) {
return (
<Section {...props}>
<ul role="list" className="space-y-16">
{children}
</ul>
</Section>
)
}
function Tool({
title,
href,
children,
}: {
title: string
href?: string
children: React.ReactNode
}) {
return (
<Card as="li">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Description>{children}</Card.Description>
</Card>
)
}
export const metadata = {
title: 'Uses',
description: 'Software I use, gadgets I love, and other things I recommend.',
}
export default function Uses() {
return (
<SimpleLayout
title="Software I use, gadgets I love, and other things I recommend."
intro="I get asked a lot about the things I use to build software, stay productive, or buy to fool myself into thinking Im being productive when Im really just procrastinating. Heres a big list of all of my favorite stuff."
>
<div className="space-y-20">
<ToolsSection title="Workstation">
<Tool title="16” MacBook Pro, M1 Max, 64GB RAM (2021)">
I was using an Intel-based 16 MacBook Pro prior to this and the
difference is night and day. Ive never heard the fans turn on a
single time, even under the incredibly heavy loads I put it through
with our various launch simulations.
</Tool>
<Tool title="Apple Pro Display XDR (Standard Glass)">
The only display on the market if you want something HiDPI and
bigger than 27. When youre working at planetary scale, every pixel
you can get counts.
</Tool>
<Tool title="IBM Model M SSK Industrial Keyboard">
They dont make keyboards the way they used to. I buy these any time
I see them go up for sale and keep them in storage in case I need
parts or need to retire my main.
</Tool>
<Tool title="Apple Magic Trackpad">
Something about all the gestures makes me feel like a wizard with
special powers. I really like feeling like a wizard with special
powers.
</Tool>
<Tool title="Herman Miller Aeron Chair">
If Im going to slouch in the worst ergonomic position imaginable
all day, I might as well do it in an expensive chair.
</Tool>
</ToolsSection>
<ToolsSection title="Development tools">
<Tool title="Sublime Text 4">
I dont care if its missing all of the fancy IDE features everyone
else relies on, Sublime Text is still the best text editor ever
made.
</Tool>
<Tool title="iTerm2">
Im honestly not even sure what features I get with this that arent
just part of the macOS Terminal but its what I use.
</Tool>
<Tool title="TablePlus">
Great software for working with databases. Has saved me from
building about a thousand admin interfaces for my various projects
over the years.
</Tool>
</ToolsSection>
<ToolsSection title="Design">
<Tool title="Figma">
We started using Figma as just a design tool but now its become our
virtual whiteboard for the entire company. Never would have expected
the collaboration features to be the real hook.
</Tool>
</ToolsSection>
<ToolsSection title="Productivity">
<Tool title="Alfred">
Its not the newest kid on the block but its still the fastest. The
Sublime Text of the application launcher world.
</Tool>
<Tool title="Reflect">
Using a daily notes system instead of trying to keep things
organized by topics has been super powerful for me. And with
Reflect, its still easy for me to keep all of that stuff
discoverable by topic even though all of my writing happens in the
daily note.
</Tool>
<Tool title="SavvyCal">
Great tool for scheduling meetings while protecting my calendar and
making sure I still have lots of time for deep work during the week.
</Tool>
<Tool title="Focus">
Simple tool for blocking distracting websites when I need to just do
the work and get some momentum going.
</Tool>
</ToolsSection>
</div>
</SimpleLayout>
)
}