Fresh template from Tailwind

This commit is contained in:
Tony Grosinger 2023-11-15 19:42:05 -08:00
commit e2e9ab6774
60 changed files with 20704 additions and 0 deletions

1
.env.example Normal file
View File

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

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

129
LICENSE.md Normal file
View File

@ -0,0 +1,129 @@
# Tailwind UI License
## Personal License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates.
- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets.
- Share your access to the Components and Templates with any other individuals.
- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a personal website by yourself.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available.
Examples of usage **not allowed** by the license:
- Creating a repository of your favorite Tailwind UI components or templates (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
- Create a Figma or Sketch UI kit based on the Tailwind UI component designs.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
In simple terms, use Tailwind UI for anything you like as long as it doesn't compete with Tailwind UI.
### Personal License Definitions
Licensee is the individual who has purchased a Personal License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Team License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates.
- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product.
- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee.
- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a website for your company.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available.
Examples of use **not allowed** by the license:
- Creating a repository of your favorite Tailwind UI components or template (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
- Creating a theme or template using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind UI license to build their own websites or side projects.
### Team License Definitions
Licensee is the business entity who has purchased a Team License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Employee is a full-time or part-time employee of the Licensee.
Contractor is an individual or business entity contracted to perform services for the Licensee.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Enforcement
If you are found to be in violation of the license, access to your Tailwind UI account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued.
The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license.
## Liability
Tailwind Labs Inc.s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates.
This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order.
## Questions?
Unsure which license you need, or unsure if your use case is covered by our licenses?
Email us at [support@tailwindui.com](mailto:support@tailwindui.com) with your questions.

42
README.md Normal file
View File

@ -0,0 +1,42 @@
# Spotlight
Spotlight is a [Tailwind UI](https://tailwindui.com) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org).
## Getting started
To get started with this template, first install the npm dependencies:
```bash
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
npm run dev
```
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
## Customizing
You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files.
## License
This site template is a commercial product and is licensed under the [Tailwind UI license](https://tailwindui.com/license).
## Learn more
To learn more about the technologies used in this site template, see the following resources:
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
- [Next.js](https://nextjs.org/docs) - the official Next.js documentation
- [Headless UI](https://headlessui.dev) - the official Headless UI documentation
- [MDX](https://mdxjs.com) - the MDX documentation

7
mdx-components.tsx Normal file
View File

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

18
next.config.mjs Normal file
View File

@ -0,0 +1,18 @@
import rehypePrism from '@mapbox/rehype-prism'
import nextMDX from '@next/mdx'
import remarkGfm from 'remark-gfm'
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
}
const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrism],
},
})
export default withMDX(nextConfig)

17617
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "tailwindui-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"@headlessui/react": "^1.7.13",
"@mapbox/rehype-prism": "^0.8.0",
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@next/mdx": "13.4.16",
"@tailwindcss/typography": "^0.5.4",
"@types/node": "20.4.7",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"@types/webpack-env": "^1.18.1",
"autoprefixer": "^10.4.12",
"cheerio": "^1.0.0-rc.12",
"clsx": "^1.2.1",
"fast-glob": "^3.2.11",
"feed": "^4.2.2",
"next": "13.4.16",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"remark-gfm": "^3.0.1",
"tailwindcss": "^3.3.3",
"typescript": "5.1.6"
},
"devDependencies": {
"eslint": "8.45.0",
"eslint-config-next": "13.4.16",
"prettier": "^3.0.1",
"prettier-plugin-tailwindcss": "^0.5.2",
"sharp": "^0.32.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

6
prettier.config.js Normal file
View File

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

130
src/app/about/page.tsx Normal file
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>
)
}

View File

@ -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

View File

@ -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
src/app/articles/page.tsx Normal file
View File

@ -0,0 +1,60 @@
import { type Metadata } from 'next'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import { type ArticleWithSlug, getAllArticles } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
function Article({ article }: { article: ArticleWithSlug }) {
return (
<article className="md:grid md:grid-cols-4 md:items-baseline">
<Card className="md:col-span-3">
<Card.Title href={`/articles/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="md:hidden"
decorate
>
{formatDate(article.date)}
</Card.Eyebrow>
<Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta>
</Card>
<Card.Eyebrow
as="time"
dateTime={article.date}
className="mt-1 hidden md:block"
>
{formatDate(article.date)}
</Card.Eyebrow>
</article>
)
}
export const metadata: Metadata = {
title: 'Articles',
description:
'All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order.',
}
export default async function ArticlesIndex() {
let articles = await getAllArticles()
return (
<SimpleLayout
title="Writing on 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>
)
}

View File

@ -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.

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

70
src/app/feed.xml/route.ts Normal file
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
src/app/layout.tsx Normal file
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
src/app/not-found.tsx Normal file
View File

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

324
src/app/page.tsx Normal file
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
src/app/projects/page.tsx Normal file
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
src/app/providers.tsx Normal file
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
src/app/speaking/page.tsx Normal file
View File

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

View File