Add initial spotlight template
1
.env.example
Normal file
@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_SITE_URL=https://example.com
|
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
35
.gitignore
vendored
Normal 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*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# generated files
|
||||
/public/rss/
|
129
LICENSE.md
Normal 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
@ -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
|
8
jsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
22
next.config.mjs
Normal file
@ -0,0 +1,22 @@
|
||||
import nextMDX from '@next/mdx'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypePrism from '@mapbox/rehype-prism'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ['js', 'jsx', 'mdx'],
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
scrollRestoration: true,
|
||||
},
|
||||
}
|
||||
|
||||
const withMDX = nextMDX({
|
||||
extension: /\.mdx?$/,
|
||||
options: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [rehypePrism],
|
||||
},
|
||||
})
|
||||
|
||||
export default withMDX(nextConfig)
|
15439
package-lock.json
generated
Normal file
38
package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"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.1.5",
|
||||
"@mdx-js/react": "^2.1.5",
|
||||
"@next/mdx": "^13.0.2",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"clsx": "^1.2.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"feed": "^4.2.2",
|
||||
"focus-visible": "^5.2.0",
|
||||
"next": "13.4.2",
|
||||
"next-router-mock": "^0.9.3",
|
||||
"postcss-focus-visible": "^6.0.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"tailwindcss": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-next": "13.0.2",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-tailwindcss": "^0.2.6"
|
||||
}
|
||||
}
|
9
postcss.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'postcss-focus-visible': {
|
||||
replaceWith: '[data-focus-visible-added]',
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
5
prettier.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
plugins: [require('prettier-plugin-tailwindcss')],
|
||||
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
72
src/components/ArticleLayout.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
import { Prose } from '@/components/Prose'
|
||||
import { formatDate } from '@/lib/formatDate'
|
||||
|
||||
function ArrowLeftIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M7.25 11.25 3.75 8m0 0 3.5-3.25M3.75 8h8.5"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArticleLayout({
|
||||
children,
|
||||
meta,
|
||||
isRssFeed = false,
|
||||
previousPathname,
|
||||
}) {
|
||||
let router = useRouter()
|
||||
|
||||
if (isRssFeed) {
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{`${meta.title} - Spencer Sharp`}</title>
|
||||
<meta name="description" content={meta.description} />
|
||||
</Head>
|
||||
<Container className="mt-16 lg:mt-32">
|
||||
<div className="xl:relative">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
{previousPathname && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
aria-label="Go back to articles"
|
||||
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
|
||||
>
|
||||
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />
|
||||
</button>
|
||||
)}
|
||||
<article>
|
||||
<header className="flex flex-col">
|
||||
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
{meta.title}
|
||||
</h1>
|
||||
<time
|
||||
dateTime={meta.date}
|
||||
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
|
||||
>
|
||||
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||
<span className="ml-3">{formatDate(meta.date)}</span>
|
||||
</time>
|
||||
</header>
|
||||
<Prose className="mt-8">{children}</Prose>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
23
src/components/Button.jsx
Normal file
@ -0,0 +1,23 @@
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const variantStyles = {
|
||||
primary:
|
||||
'bg-zinc-800 font-semibold text-zinc-100 hover:bg-zinc-700 active:bg-zinc-800 active:text-zinc-100/70 dark:bg-zinc-700 dark:hover:bg-zinc-600 dark:active:bg-zinc-700 dark:active:text-zinc-100/70',
|
||||
secondary:
|
||||
'bg-zinc-50 font-medium text-zinc-900 hover:bg-zinc-100 active:bg-zinc-100 active:text-zinc-900/60 dark:bg-zinc-800/50 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:active:bg-zinc-800/50 dark:active:text-zinc-50/70',
|
||||
}
|
||||
|
||||
export function Button({ variant = 'primary', className, href, ...props }) {
|
||||
className = clsx(
|
||||
'inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none',
|
||||
variantStyles[variant],
|
||||
className
|
||||
)
|
||||
|
||||
return href ? (
|
||||
<Link href={href} className={className} {...props} />
|
||||
) : (
|
||||
<button className={className} {...props} />
|
||||
)
|
||||
}
|
94
src/components/Card.jsx
Normal file
@ -0,0 +1,94 @@
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
function ChevronRightIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Card({ as: Component = 'div', className, children }) {
|
||||
return (
|
||||
<Component
|
||||
className={clsx(className, 'group relative flex flex-col items-start')}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Link = function CardLink({ children, ...props }) {
|
||||
return (
|
||||
<>
|
||||
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||
<Link {...props}>
|
||||
<span className="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||
<span className="relative z-10">{children}</span>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Title = function CardTitle({ as: Component = 'h2', href, children }) {
|
||||
return (
|
||||
<Component className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Description = function CardDescription({ children }) {
|
||||
return (
|
||||
<p className="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Cta = function CardCta({ children }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative z-10 mt-4 flex items-center text-sm font-medium text-teal-500"
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-1 h-4 w-4 stroke-current" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Eyebrow = function CardEyebrow({
|
||||
as: Component = 'p',
|
||||
decorate = false,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
className,
|
||||
'relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500',
|
||||
decorate && 'pl-3.5'
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{decorate && (
|
||||
<span
|
||||
className="absolute inset-y-0 left-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
42
src/components/Container.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { forwardRef } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const OuterContainer = forwardRef(function OuterContainer(
|
||||
{ className, children, ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div ref={ref} className={clsx('sm:px-8', className)} {...props}>
|
||||
<div className="mx-auto max-w-7xl lg:px-8">{children}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const InnerContainer = forwardRef(function InnerContainer(
|
||||
{ className, children, ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx('relative px-4 sm:px-8 lg:px-12', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto max-w-2xl lg:max-w-5xl">{children}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export const Container = forwardRef(function Container(
|
||||
{ children, ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<OuterContainer ref={ref} {...props}>
|
||||
<InnerContainer>{children}</InnerContainer>
|
||||
</OuterContainer>
|
||||
)
|
||||
})
|
||||
|
||||
Container.Outer = OuterContainer
|
||||
Container.Inner = InnerContainer
|
39
src/components/Footer.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
|
||||
function NavLink({ href, children }) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="transition hover:text-teal-500 dark:hover:text-teal-400"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="mt-32">
|
||||
<Container.Outer>
|
||||
<div className="border-t border-zinc-100 pb-16 pt-10 dark:border-zinc-700/40">
|
||||
<Container.Inner>
|
||||
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
<NavLink href="/about">About</NavLink>
|
||||
<NavLink href="/projects">Projects</NavLink>
|
||||
<NavLink href="/speaking">Speaking</NavLink>
|
||||
<NavLink href="/uses">Uses</NavLink>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400 dark:text-zinc-500">
|
||||
© {new Date().getFullYear()} Spencer Sharp. All rights
|
||||
reserved.
|
||||
</p>
|
||||
</div>
|
||||
</Container.Inner>
|
||||
</div>
|
||||
</Container.Outer>
|
||||
</footer>
|
||||
)
|
||||
}
|
427
src/components/Header.jsx
Normal file
@ -0,0 +1,427 @@
|
||||
import { Fragment, useEffect, useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
import avatarImage from '@/images/avatar.jpg'
|
||||
|
||||
function CloseIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="m17.25 6.75-10.5 10.5M6.75 6.75l10.5 10.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronDownIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 8 6" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M1.75 1.75 4 4.25l2.25-2.5"
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SunIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M8 12.25A4.25 4.25 0 0 1 12.25 8v0a4.25 4.25 0 0 1 4.25 4.25v0a4.25 4.25 0 0 1-4.25 4.25v0A4.25 4.25 0 0 1 8 12.25v0Z" />
|
||||
<path
|
||||
d="M12.25 3v1.5M21.5 12.25H20M18.791 18.791l-1.06-1.06M18.791 5.709l-1.06 1.06M12.25 20v1.5M4.5 12.25H3M6.77 6.77 5.709 5.709M6.77 17.73l-1.061 1.061"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MoonIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M17.25 16.22a6.937 6.937 0 0 1-9.47-9.47 7.451 7.451 0 1 0 9.47 9.47ZM12.75 7C17 7 17 2.75 17 2.75S17 7 21.25 7C17 7 17 11.25 17 11.25S17 7 12.75 7Z"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileNavItem({ href, children }) {
|
||||
return (
|
||||
<li>
|
||||
<Popover.Button as={Link} href={href} className="block py-2">
|
||||
{children}
|
||||
</Popover.Button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileNavigation(props) {
|
||||
return (
|
||||
<Popover {...props}>
|
||||
<Popover.Button className="group flex items-center rounded-full bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
|
||||
Menu
|
||||
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700 dark:group-hover:stroke-zinc-400" />
|
||||
</Popover.Button>
|
||||
<Transition.Root>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="duration-150 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-150 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Overlay className="fixed inset-0 z-50 bg-zinc-800/40 backdrop-blur-sm dark:bg-black/80" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="duration-150 ease-out"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="duration-150 ease-in"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel
|
||||
focus
|
||||
className="fixed inset-x-4 top-8 z-50 origin-top rounded-3xl bg-white p-8 ring-1 ring-zinc-900/5 dark:bg-zinc-900 dark:ring-zinc-800"
|
||||
>
|
||||
<div className="flex flex-row-reverse items-center justify-between">
|
||||
<Popover.Button aria-label="Close menu" className="-m-1 p-1">
|
||||
<CloseIcon className="h-6 w-6 text-zinc-500 dark:text-zinc-400" />
|
||||
</Popover.Button>
|
||||
<h2 className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||
Navigation
|
||||
</h2>
|
||||
</div>
|
||||
<nav className="mt-6">
|
||||
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800 dark:divide-zinc-100/5 dark:text-zinc-300">
|
||||
<MobileNavItem href="/about">About</MobileNavItem>
|
||||
<MobileNavItem href="/articles">Articles</MobileNavItem>
|
||||
<MobileNavItem href="/projects">Projects</MobileNavItem>
|
||||
<MobileNavItem href="/speaking">Speaking</MobileNavItem>
|
||||
<MobileNavItem href="/uses">Uses</MobileNavItem>
|
||||
</ul>
|
||||
</nav>
|
||||
</Popover.Panel>
|
||||
</Transition.Child>
|
||||
</Transition.Root>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function NavItem({ href, children }) {
|
||||
let isActive = useRouter().pathname === href
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
href={href}
|
||||
className={clsx(
|
||||
'relative block px-3 py-2 transition',
|
||||
isActive
|
||||
? 'text-teal-500 dark:text-teal-400'
|
||||
: 'hover:text-teal-500 dark:hover:text-teal-400'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{isActive && (
|
||||
<span className="absolute inset-x-1 -bottom-px h-px bg-gradient-to-r from-teal-500/0 via-teal-500/40 to-teal-500/0 dark:from-teal-400/0 dark:via-teal-400/40 dark:to-teal-400/0" />
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopNavigation(props) {
|
||||
return (
|
||||
<nav {...props}>
|
||||
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
|
||||
<NavItem href="/about">About</NavItem>
|
||||
<NavItem href="/articles">Articles</NavItem>
|
||||
<NavItem href="/projects">Projects</NavItem>
|
||||
<NavItem href="/speaking">Speaking</NavItem>
|
||||
<NavItem href="/uses">Uses</NavItem>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function ModeToggle() {
|
||||
function disableTransitionsTemporarily() {
|
||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||
window.setTimeout(() => {
|
||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
disableTransitionsTemporarily()
|
||||
|
||||
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
let isSystemDarkMode = darkModeMediaQuery.matches
|
||||
let isDarkMode = document.documentElement.classList.toggle('dark')
|
||||
|
||||
if (isDarkMode === isSystemDarkMode) {
|
||||
delete window.localStorage.isDarkMode
|
||||
} else {
|
||||
window.localStorage.isDarkMode = isDarkMode
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle dark mode"
|
||||
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
|
||||
onClick={toggleMode}
|
||||
>
|
||||
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 dark:hidden [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-teal-500 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-teal-600" />
|
||||
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition dark:block [@media(prefers-color-scheme:dark)]:group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function clamp(number, a, b) {
|
||||
let min = Math.min(a, b)
|
||||
let max = Math.max(a, b)
|
||||
return Math.min(Math.max(number, min), max)
|
||||
}
|
||||
|
||||
function AvatarContainer({ className, ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className,
|
||||
'h-10 w-10 rounded-full bg-white/90 p-0.5 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:ring-white/10'
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Avatar({ large = false, className, ...props }) {
|
||||
return (
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Home"
|
||||
className={clsx(className, 'pointer-events-auto')}
|
||||
{...props}
|
||||
>
|
||||
<Image
|
||||
src={avatarImage}
|
||||
alt=""
|
||||
sizes={large ? '4rem' : '2.25rem'}
|
||||
className={clsx(
|
||||
'rounded-full bg-zinc-100 object-cover dark:bg-zinc-800',
|
||||
large ? 'h-16 w-16' : 'h-9 w-9'
|
||||
)}
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
let isHomePage = useRouter().pathname === '/'
|
||||
|
||||
let headerRef = useRef()
|
||||
let avatarRef = useRef()
|
||||
let isInitial = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
let downDelay = avatarRef.current?.offsetTop ?? 0
|
||||
let upDelay = 64
|
||||
|
||||
function setProperty(property, value) {
|
||||
document.documentElement.style.setProperty(property, value)
|
||||
}
|
||||
|
||||
function removeProperty(property) {
|
||||
document.documentElement.style.removeProperty(property)
|
||||
}
|
||||
|
||||
function updateHeaderStyles() {
|
||||
let { top, height } = headerRef.current.getBoundingClientRect()
|
||||
let scrollY = clamp(
|
||||
window.scrollY,
|
||||
0,
|
||||
document.body.scrollHeight - window.innerHeight
|
||||
)
|
||||
|
||||
if (isInitial.current) {
|
||||
setProperty('--header-position', 'sticky')
|
||||
}
|
||||
|
||||
setProperty('--content-offset', `${downDelay}px`)
|
||||
|
||||
if (isInitial.current || scrollY < downDelay) {
|
||||
setProperty('--header-height', `${downDelay + height}px`)
|
||||
setProperty('--header-mb', `${-downDelay}px`)
|
||||
} else if (top + height < -upDelay) {
|
||||
let offset = Math.max(height, scrollY - upDelay)
|
||||
setProperty('--header-height', `${offset}px`)
|
||||
setProperty('--header-mb', `${height - offset}px`)
|
||||
} else if (top === 0) {
|
||||
setProperty('--header-height', `${scrollY + height}px`)
|
||||
setProperty('--header-mb', `${-scrollY}px`)
|
||||
}
|
||||
|
||||
if (top === 0 && scrollY > 0 && scrollY >= downDelay) {
|
||||
setProperty('--header-inner-position', 'fixed')
|
||||
removeProperty('--header-top')
|
||||
removeProperty('--avatar-top')
|
||||
} else {
|
||||
removeProperty('--header-inner-position')
|
||||
setProperty('--header-top', '0px')
|
||||
setProperty('--avatar-top', '0px')
|
||||
}
|
||||
}
|
||||
|
||||
function updateAvatarStyles() {
|
||||
if (!isHomePage) {
|
||||
return
|
||||
}
|
||||
|
||||
let fromScale = 1
|
||||
let toScale = 36 / 64
|
||||
let fromX = 0
|
||||
let toX = 2 / 16
|
||||
|
||||
let scrollY = downDelay - window.scrollY
|
||||
|
||||
let scale = (scrollY * (fromScale - toScale)) / downDelay + toScale
|
||||
scale = clamp(scale, fromScale, toScale)
|
||||
|
||||
let x = (scrollY * (fromX - toX)) / downDelay + toX
|
||||
x = clamp(x, fromX, toX)
|
||||
|
||||
setProperty(
|
||||
'--avatar-image-transform',
|
||||
`translate3d(${x}rem, 0, 0) scale(${scale})`
|
||||
)
|
||||
|
||||
let borderScale = 1 / (toScale / scale)
|
||||
let borderX = (-toX + x) * borderScale
|
||||
let borderTransform = `translate3d(${borderX}rem, 0, 0) scale(${borderScale})`
|
||||
|
||||
setProperty('--avatar-border-transform', borderTransform)
|
||||
setProperty('--avatar-border-opacity', scale === toScale ? 1 : 0)
|
||||
}
|
||||
|
||||
function updateStyles() {
|
||||
updateHeaderStyles()
|
||||
updateAvatarStyles()
|
||||
isInitial.current = false
|
||||
}
|
||||
|
||||
updateStyles()
|
||||
window.addEventListener('scroll', updateStyles, { passive: true })
|
||||
window.addEventListener('resize', updateStyles)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateStyles)
|
||||
window.removeEventListener('resize', updateStyles)
|
||||
}
|
||||
}, [isHomePage])
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className="pointer-events-none relative z-50 flex flex-col"
|
||||
style={{
|
||||
height: 'var(--header-height)',
|
||||
marginBottom: 'var(--header-mb)',
|
||||
}}
|
||||
>
|
||||
{isHomePage && (
|
||||
<>
|
||||
<div
|
||||
ref={avatarRef}
|
||||
className="order-last mt-[calc(theme(spacing.16)-theme(spacing.3))]"
|
||||
/>
|
||||
<Container
|
||||
className="top-0 order-last -mb-3 pt-3"
|
||||
style={{ position: 'var(--header-position)' }}
|
||||
>
|
||||
<div
|
||||
className="top-[var(--avatar-top,theme(spacing.3))] w-full"
|
||||
style={{ position: 'var(--header-inner-position)' }}
|
||||
>
|
||||
<div className="relative">
|
||||
<AvatarContainer
|
||||
className="absolute left-0 top-3 origin-left transition-opacity"
|
||||
style={{
|
||||
opacity: 'var(--avatar-border-opacity, 0)',
|
||||
transform: 'var(--avatar-border-transform)',
|
||||
}}
|
||||
/>
|
||||
<Avatar
|
||||
large
|
||||
className="block h-16 w-16 origin-left"
|
||||
style={{ transform: 'var(--avatar-image-transform)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
ref={headerRef}
|
||||
className="top-0 z-10 h-16 pt-6"
|
||||
style={{ position: 'var(--header-position)' }}
|
||||
>
|
||||
<Container
|
||||
className="top-[var(--header-top,theme(spacing.6))] w-full"
|
||||
style={{ position: 'var(--header-inner-position)' }}
|
||||
>
|
||||
<div className="relative flex gap-4">
|
||||
<div className="flex flex-1">
|
||||
{!isHomePage && (
|
||||
<AvatarContainer>
|
||||
<Avatar />
|
||||
</AvatarContainer>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 justify-end md:justify-center">
|
||||
<MobileNavigation className="pointer-events-auto md:hidden" />
|
||||
<DesktopNavigation className="pointer-events-auto hidden md:block" />
|
||||
</div>
|
||||
<div className="flex justify-end md:flex-1">
|
||||
<div className="pointer-events-auto">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</header>
|
||||
{isHomePage && <div style={{ height: 'var(--content-offset)' }} />}
|
||||
</>
|
||||
)
|
||||
}
|
7
src/components/Prose.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
export function Prose({ children, className }) {
|
||||
return (
|
||||
<div className={clsx(className, 'prose dark:prose-invert')}>{children}</div>
|
||||
)
|
||||
}
|
22
src/components/Section.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { useId } from 'react'
|
||||
|
||||
export function Section({ title, children }) {
|
||||
let id = useId()
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby={id}
|
||||
className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40"
|
||||
>
|
||||
<div className="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
|
||||
<h2
|
||||
id={id}
|
||||
className="text-sm font-semibold text-zinc-800 dark:text-zinc-100"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<div className="md:col-span-3">{children}</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
17
src/components/SimpleLayout.jsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Container } from '@/components/Container'
|
||||
|
||||
export function SimpleLayout({ title, intro, children }) {
|
||||
return (
|
||||
<Container className="mt-16 sm:mt-32">
|
||||
<header className="max-w-2xl">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
|
||||
{intro}
|
||||
</p>
|
||||
</header>
|
||||
<div className="mt-16 sm:mt-20">{children}</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
36
src/components/SocialIcons.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
export function TwitterIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path d="M20.055 7.983c.011.174.011.347.011.523 0 5.338-3.92 11.494-11.09 11.494v-.003A10.755 10.755 0 0 1 3 18.186c.308.038.618.057.928.058a7.655 7.655 0 0 0 4.841-1.733c-1.668-.032-3.13-1.16-3.642-2.805a3.753 3.753 0 0 0 1.76-.07C5.07 13.256 3.76 11.6 3.76 9.676v-.05a3.77 3.77 0 0 0 1.77.505C3.816 8.945 3.288 6.583 4.322 4.737c1.98 2.524 4.9 4.058 8.034 4.22a4.137 4.137 0 0 1 1.128-3.86A3.807 3.807 0 0 1 19 5.274a7.657 7.657 0 0 0 2.475-.98c-.29.934-.9 1.729-1.713 2.233A7.54 7.54 0 0 0 22 5.89a8.084 8.084 0 0 1-1.945 2.093Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function InstagramIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path d="M12 3c-2.444 0-2.75.01-3.71.054-.959.044-1.613.196-2.185.418A4.412 4.412 0 0 0 4.51 4.511c-.5.5-.809 1.002-1.039 1.594-.222.572-.374 1.226-.418 2.184C3.01 9.25 3 9.556 3 12s.01 2.75.054 3.71c.044.959.196 1.613.418 2.185.23.592.538 1.094 1.039 1.595.5.5 1.002.808 1.594 1.038.572.222 1.226.374 2.184.418C9.25 20.99 9.556 21 12 21s2.75-.01 3.71-.054c.959-.044 1.613-.196 2.185-.419a4.412 4.412 0 0 0 1.595-1.038c.5-.5.808-1.002 1.038-1.594.222-.572.374-1.226.418-2.184.044-.96.054-1.267.054-3.711s-.01-2.75-.054-3.71c-.044-.959-.196-1.613-.419-2.185A4.412 4.412 0 0 0 19.49 4.51c-.5-.5-1.002-.809-1.594-1.039-.572-.222-1.226-.374-2.184-.418C14.75 3.01 14.444 3 12 3Zm0 1.622c2.403 0 2.688.009 3.637.052.877.04 1.354.187 1.67.31.421.163.72.358 1.036.673.315.315.51.615.673 1.035.123.317.27.794.31 1.671.043.95.052 1.234.052 3.637s-.009 2.688-.052 3.637c-.04.877-.187 1.354-.31 1.67-.163.421-.358.72-.673 1.036a2.79 2.79 0 0 1-1.035.673c-.317.123-.794.27-1.671.31-.95.043-1.234.052-3.637.052s-2.688-.009-3.637-.052c-.877-.04-1.354-.187-1.67-.31a2.789 2.789 0 0 1-1.036-.673 2.79 2.79 0 0 1-.673-1.035c-.123-.317-.27-.794-.31-1.671-.043-.95-.052-1.234-.052-3.637s.009-2.688.052-3.637c.04-.877.187-1.354.31-1.67.163-.421.358-.72.673-1.036.315-.315.615-.51 1.035-.673.317-.123.794-.27 1.671-.31.95-.043 1.234-.052 3.637-.052Z" />
|
||||
<path d="M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6Zm0-7.622a4.622 4.622 0 1 0 0 9.244 4.622 4.622 0 0 0 0-9.244Zm5.884-.182a1.08 1.08 0 1 1-2.16 0 1.08 1.08 0 0 1 2.16 0Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GitHubIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.475 2 2 6.588 2 12.253c0 4.537 2.862 8.369 6.838 9.727.5.09.687-.218.687-.487 0-.243-.013-1.05-.013-1.91C7 20.059 6.35 18.957 6.15 18.38c-.113-.295-.6-1.205-1.025-1.448-.35-.192-.85-.667-.013-.68.788-.012 1.35.744 1.538 1.051.9 1.551 2.338 1.116 2.912.846.088-.666.35-1.115.638-1.371-2.225-.256-4.55-1.14-4.55-5.062 0-1.115.387-2.038 1.025-2.756-.1-.256-.45-1.307.1-2.717 0 0 .837-.269 2.75 1.051.8-.23 1.65-.346 2.5-.346.85 0 1.7.115 2.5.346 1.912-1.333 2.75-1.05 2.75-1.05.55 1.409.2 2.46.1 2.716.637.718 1.025 1.628 1.025 2.756 0 3.934-2.337 4.806-4.562 5.062.362.32.675.936.675 1.897 0 1.371-.013 2.473-.013 2.82 0 .268.188.589.688.486a10.039 10.039 0 0 0 4.932-3.74A10.447 10.447 0 0 0 22 12.253C22 6.588 17.525 2 12 2Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function LinkedInIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path d="M18.335 18.339H15.67v-4.177c0-.996-.02-2.278-1.39-2.278-1.389 0-1.601 1.084-1.601 2.205v4.25h-2.666V9.75h2.56v1.17h.035c.358-.674 1.228-1.387 2.528-1.387 2.7 0 3.2 1.778 3.2 4.091v4.715zM7.003 8.575a1.546 1.546 0 01-1.548-1.549 1.548 1.548 0 111.547 1.549zm1.336 9.764H5.666V9.75H8.34v8.589zM19.67 3H4.329C3.593 3 3 3.58 3 4.297v15.406C3 20.42 3.594 21 4.328 21h15.338C20.4 21 21 20.42 21 19.703V4.297C21 3.58 20.4 3 19.666 3h.003z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
BIN
src/images/avatar.jpg
Normal file
After Width: | Height: | Size: 237 KiB |
13
src/images/logos/airbnb.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="28" height="28" rx="14" fill="#FF5A5F" />
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
d="M14.001 18.183c-.902-1.131-1.432-2.122-1.609-2.971-.175-.685-.106-1.232.194-1.643.318-.474.792-.704 1.414-.704.622 0 1.096.23 1.414.708.297.407.372.955.19 1.644-.194.865-.723 1.856-1.608 2.972l.005-.006Zm6.4.76c-.123.831-.689 1.52-1.466 1.856-1.502.653-2.989-.389-4.261-1.803 2.104-2.634 2.493-4.685 1.59-6.012-.53-.76-1.289-1.13-2.263-1.13-1.963 0-3.042 1.66-2.618 3.588.247 1.043.901 2.229 1.945 3.555-.654.723-1.274 1.237-1.822 1.555-.424.23-.83.372-1.218.406-1.786.266-3.186-1.467-2.55-3.253.088-.23.263-.654.563-1.308l.017-.035c.976-2.119 2.161-4.527 3.523-7.197l.035-.088.387-.744c.3-.548.423-.793.9-1.095.231-.14.514-.21.831-.21.636 0 1.132.372 1.344.671.106.16.23.372.388.636l.372.726.054.106c1.36 2.669 2.547 5.072 3.52 7.196l.016.016.356.814.212.509c.162.409.196.815.142 1.239l.004.002Zm.814-1.593c-.124-.389-.337-.847-.6-1.396v-.02a235.28 235.28 0 0 0-3.538-7.23l-.074-.108C16.212 6.974 15.646 6 14.001 6c-1.627 0-2.317 1.13-3.023 2.599l-.054.106a221.31 221.31 0 0 0-3.536 7.232v.035l-.372.813c-.14.336-.212.512-.23.565-.9 2.477.955 4.65 3.201 4.65.018 0 .088 0 .177-.018h.248c1.166-.142 2.37-.883 3.59-2.211 1.218 1.326 2.422 2.069 3.587 2.211h.248c.089.018.16.018.177.018 2.246.002 4.101-2.174 3.201-4.65Z"
|
||||
fill="#fff" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" transform="translate(6 6)" d="M0 0h16v16H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
12
src/images/logos/animaginary.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="16" fill="url(#a)"/>
|
||||
<rect x="16" y="8" width="8" height="8" rx="2" fill="#fff" fill-opacity=".4"/>
|
||||
<rect x="12" y="12" width="8" height="8" rx="2" fill="#fff" fill-opacity=".5"/>
|
||||
<rect x="8" y="16" width="8" height="8" rx="2" fill="#fff"/>
|
||||
<defs>
|
||||
<radialGradient id="a" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 18.5 -18.5 0 16 13.5)">
|
||||
<stop offset=".169" stop-color="#22D3EE"/>
|
||||
<stop offset="1" stop-color="#7451FF"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 625 B |
25
src/images/logos/cosmos.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)">
|
||||
<rect width="32" height="32" rx="16" fill="#001120"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 13a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-2Zm-3 1a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-2a3 3 0 0 1-3-3v-4Zm10-.257A2.743 2.743 0 0 1 19.743 11H22a3 3 0 0 1 3 3 1 1 0 1 1-2 0 1 1 0 0 0-1-1h-2.257a.743.743 0 0 0-.235 1.449l3.616 1.205A2.743 2.743 0 0 1 22.257 21H20a3 3 0 0 1-3-3 1 1 0 1 1 2 0 1 1 0 0 0 1 1h2.257a.743.743 0 0 0 .235-1.449l-3.616-1.205A2.743 2.743 0 0 1 17 13.743Z" fill="url(#b)"/>
|
||||
<path fill="#fff" fill-opacity=".1" d="M0 23h32v1H0z"/>
|
||||
<path fill="#fff" fill-opacity=".1" d="M5 0v32H4V0z"/>
|
||||
<path fill="#fff" fill-opacity=".1" d="M0 8h32v1H0z"/>
|
||||
<path fill="url(#c)" d="M0 23h32v1H0z"/>
|
||||
<path fill="#fff" fill-opacity=".1" d="M28 0v32h-1V0z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="11" y1="12" x2="11" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff"/>
|
||||
<stop offset="1" stop-color="#E3E8ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="1.5" y1="23.5" x2="30.5" y2="23.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff" stop-opacity="0"/>
|
||||
<stop offset=".486" stop-color="#fff"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<rect width="32" height="32" rx="16" fill="#fff"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
18
src/images/logos/facebook.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)">
|
||||
<rect width="28" height="28" rx="14" fill="#fff" />
|
||||
<g clip-path="url(#b)">
|
||||
<path
|
||||
d="M29 14.09c0-8.283-6.716-15-15-15-8.284 0-15 6.717-15 15 0 7.488 5.485 13.693 12.656 14.818v-10.48H7.847V14.09h3.81v-3.303c0-3.759 2.24-5.836 5.666-5.836 1.64 0 3.357.294 3.357.294v3.69h-1.892c-1.864 0-2.445 1.157-2.445 2.343v2.813h4.16l-.666 4.337h-3.494V28.91C23.515 27.783 29 21.577 29 14.09Z"
|
||||
fill="#1877F2" />
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<rect width="28" height="28" rx="14" fill="#fff" />
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path fill="#fff" transform="translate(-1 -1)" d="M0 0h30v30H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 781 B |
5
src/images/logos/helio-stream.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="16" fill="#F43F5E" />
|
||||
<path d="M9 11a2 2 0 0 1 2-2h2v12a2 2 0 0 1-2 2H9V11ZM19 11a2 2 0 0 1 2-2h2v12a2 2 0 0 1-2 2h-2V11Z" fill="#fff" />
|
||||
<path d="M15.447 16.106A2 2 0 0 1 17.237 15H21v2h-6l.447-.894Z" fill="#fff" />
|
||||
</svg>
|
After Width: | Height: | Size: 339 B |
21
src/images/logos/open-shuttle.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)">
|
||||
<rect width="32" height="32" rx="16" fill="#0085FF" />
|
||||
<path fill="#fff" fill-opacity=".2" d="M0 26h32v1H0z" />
|
||||
<path fill="#fff" fill-opacity=".2" d="M8 0v32H7V0zM16.5 0v32h-1V0z" />
|
||||
<path fill="#fff" fill-opacity=".2" d="M0 5h32v1H0zM0 15.5h32v1H0z" />
|
||||
<path fill="#fff" fill-opacity=".2" d="M25 0v32h-1V0z" />
|
||||
<path
|
||||
d="M13 21v-5.485c0-2.959-.228-4.866 1.24-7.435l.892-1.56a1 1 0 0 1 1.736 0l.892 1.56C19.228 10.65 19 12.556 19 15.515V21l3.293 3.293c.63.63.184 1.707-.707 1.707H10.414c-.89 0-1.337-1.077-.707-1.707L13 21Z"
|
||||
fill="#fff" />
|
||||
<path
|
||||
d="M13 26v-5m0 5h6m-6 0h-2.586c-.89 0-1.337-1.077-.707-1.707L13 21m0 0v-5.485c0-2.959-.228-4.866 1.24-7.435l.892-1.56a1 1 0 0 1 1.736 0l.892 1.56C19.228 10.65 19 12.556 19 15.515V21m0 5h2.586c.89 0 1.337-1.077.707-1.707L19 21m0 5v-5"
|
||||
stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M16 13v2" stroke="#0085FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<rect width="32" height="32" rx="16" fill="#fff" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
24
src/images/logos/planetaria.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#a)">
|
||||
<rect width="32" height="32" rx="16" fill="url(#b)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.755 30.136c.051-4.308.484-8.167 1.147-10.985.355-1.509.766-2.677 1.196-3.45.466-.838.801-.951.902-.951.1 0 .436.113.902.95.43.774.841 1.942 1.196 3.451.663 2.818 1.096 6.677 1.147 10.985.508-.117 1.007-.26 1.494-.428-.074-4.224-.508-8.042-1.18-10.9-.327-1.389-.718-2.584-1.169-3.497 1.498.715 2.887 2.097 4.035 4.048 1.257 2.138 2.179 4.894 2.589 8 .47-.374.916-.775 1.335-1.203-.488-2.878-1.402-5.467-2.631-7.557-.652-1.107-1.402-2.09-2.236-2.901a16.296 16.296 0 0 1 7.851 6.01c.236-.55.439-1.117.606-1.699C26.688 15.892 21.653 13.25 16 13.25c-5.653 0-10.688 2.642-13.939 6.759.167.582.37 1.15.606 1.7a16.295 16.295 0 0 1 7.85-6.011c-.833.811-1.583 1.794-2.235 2.9-1.23 2.091-2.143 4.68-2.631 7.558.42.428.866.83 1.335 1.203.41-3.106 1.332-5.862 2.59-8 1.147-1.95 2.536-3.333 4.034-4.048-.451.913-.842 2.108-1.168 3.496-.673 2.86-1.107 6.677-1.18 10.901.486.168.984.311 1.493.428Zm-.002 1.534a15.857 15.857 0 0 1-1.502-.387c.017 4.623.465 8.825 1.19 11.91.327 1.388.718 2.583 1.169 3.496-1.498-.715-2.887-2.097-4.035-4.048C7.845 39.698 6.75 35.584 6.75 31c0-.643.022-1.276.063-1.899a16.074 16.074 0 0 1-1.41-1.113c-.1.98-.153 1.986-.153 3.012 0 4.804 1.144 9.19 3.032 12.401.652 1.107 1.402 2.09 2.236 2.901C4.24 44.052-.25 38.051-.25 31c0-2.814.715-5.46 1.974-7.768a15.914 15.914 0 0 1-.741-1.699A17.667 17.667 0 0 0-1.75 31c0 9.803 7.947 17.75 17.75 17.75S33.75 40.803 33.75 31c0-3.48-1.002-6.727-2.733-9.467-.214.583-.462 1.15-.74 1.699A16.178 16.178 0 0 1 32.25 31c0 7.05-4.49 13.053-10.768 15.302.834-.811 1.584-1.794 2.236-2.9C25.605 40.19 26.75 35.803 26.75 31c0-1.026-.052-2.032-.152-3.012-.45.396-.92.768-1.411 1.113.041.623.063 1.256.063 1.899 0 4.584-1.095 8.698-2.825 11.64-1.148 1.952-2.537 3.334-4.035 4.049.451-.913.842-2.108 1.168-3.496.726-3.085 1.174-7.287 1.192-11.91-.492.153-.993.282-1.503.387-.04 4.386-.476 8.319-1.149 11.179-.355 1.509-.766 2.677-1.196 3.45-.466.838-.801.951-.902.951-.1 0-.436-.113-.902-.95-.43-.774-.841-1.942-1.196-3.451-.673-2.86-1.108-6.793-1.149-11.179Z" fill="url(#c)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.894 5.553a1 1 0 0 1-.447 1.342l-9.341 3.552c-.494.247.246.494 0 0-.247-.494-.494.247 0 0l8.447-5.341a1 1 0 0 1 1.341.447Z" fill="url(#d)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="16" y1="0" x2="16" y2="33" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00172C"/>
|
||||
<stop offset=".803" stop-color="#5900EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" x1="19" y1="6" x2="11.5" y2="10" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9969E8"/>
|
||||
<stop offset="1" stop-color="#FFA0D2" stop-opacity=".32"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 35.75 -57.5 0 16 13)">
|
||||
<stop offset=".14" stop-color="#fff"/>
|
||||
<stop offset=".514" stop-color="#fff" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<clipPath id="a">
|
||||
<rect width="32" height="32" rx="16" fill="#fff"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
18
src/images/logos/starbucks.svg
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/images/photos/image-1.jpg
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
src/images/photos/image-2.jpg
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
src/images/photos/image-3.jpg
Normal file
After Width: | Height: | Size: 2.4 MiB |
BIN
src/images/photos/image-4.jpg
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
src/images/photos/image-5.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
src/images/portrait.jpg
Normal file
After Width: | Height: | Size: 286 KiB |
8
src/lib/formatDate.js
Normal file
@ -0,0 +1,8 @@
|
||||
export function formatDate(dateString) {
|
||||
return new Date(`${dateString}T00:00:00Z`).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
}
|
56
src/lib/generateRssFeed.js
Normal file
@ -0,0 +1,56 @@
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider'
|
||||
import { Feed } from 'feed'
|
||||
import { mkdir, writeFile } from 'fs/promises'
|
||||
|
||||
import { getAllArticles } from './getAllArticles'
|
||||
|
||||
export async function generateRssFeed() {
|
||||
let articles = await getAllArticles()
|
||||
let siteUrl = process.env.NEXT_PUBLIC_SITE_URL
|
||||
let author = {
|
||||
name: '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}/rss/feed.xml`,
|
||||
json: `${siteUrl}/rss/feed.json`,
|
||||
},
|
||||
})
|
||||
|
||||
for (let article of articles) {
|
||||
let url = `${siteUrl}/articles/${article.slug}`
|
||||
let html = ReactDOMServer.renderToStaticMarkup(
|
||||
<MemoryRouterProvider>
|
||||
<article.component isRssFeed />
|
||||
</MemoryRouterProvider>
|
||||
)
|
||||
|
||||
feed.addItem({
|
||||
title: article.title,
|
||||
id: url,
|
||||
link: url,
|
||||
description: article.description,
|
||||
content: html,
|
||||
author: [author],
|
||||
contributor: [author],
|
||||
date: new Date(article.date),
|
||||
})
|
||||
}
|
||||
|
||||
await mkdir('./public/rss', { recursive: true })
|
||||
await Promise.all([
|
||||
writeFile('./public/rss/feed.xml', feed.rss2(), 'utf8'),
|
||||
writeFile('./public/rss/feed.json', feed.json1(), 'utf8'),
|
||||
])
|
||||
}
|
23
src/lib/getAllArticles.js
Normal file
@ -0,0 +1,23 @@
|
||||
import glob from 'fast-glob'
|
||||
import * as path from 'path'
|
||||
|
||||
async function importArticle(articleFilename) {
|
||||
let { meta, default: component } = await import(
|
||||
`../pages/articles/${articleFilename}`
|
||||
)
|
||||
return {
|
||||
slug: articleFilename.replace(/(\/index)?\.mdx$/, ''),
|
||||
...meta,
|
||||
component,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllArticles() {
|
||||
let articleFilenames = await glob(['*.mdx', '*/index.mdx'], {
|
||||
cwd: path.join(process.cwd(), 'src/pages/articles'),
|
||||
})
|
||||
|
||||
let articles = await Promise.all(articleFilenames.map(importArticle))
|
||||
|
||||
return articles.sort((a, z) => new Date(z.date) - new Date(a.date))
|
||||
}
|
38
src/pages/_app.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { Header } from '@/components/Header'
|
||||
|
||||
import '@/styles/tailwind.css'
|
||||
import 'focus-visible'
|
||||
|
||||
function usePrevious(value) {
|
||||
let ref = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
|
||||
return ref.current
|
||||
}
|
||||
|
||||
export default function App({ Component, pageProps, router }) {
|
||||
let previousPathname = usePrevious(router.pathname)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 flex justify-center sm:px-8">
|
||||
<div className="flex w-full max-w-7xl lg:px-8">
|
||||
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Header />
|
||||
<main>
|
||||
<Component previousPathname={previousPathname} {...pageProps} />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
60
src/pages/_document.jsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Head, Html, Main, NextScript } from 'next/document'
|
||||
|
||||
const modeScript = `
|
||||
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
updateMode()
|
||||
darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions)
|
||||
window.addEventListener('storage', updateModeWithoutTransitions)
|
||||
|
||||
function updateMode() {
|
||||
let isSystemDarkMode = darkModeMediaQuery.matches
|
||||
let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode)
|
||||
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
|
||||
if (isDarkMode === isSystemDarkMode) {
|
||||
delete window.localStorage.isDarkMode
|
||||
}
|
||||
}
|
||||
|
||||
function disableTransitionsTemporarily() {
|
||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||
window.setTimeout(() => {
|
||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function updateModeWithoutTransitions() {
|
||||
disableTransitionsTemporarily()
|
||||
updateMode()
|
||||
}
|
||||
`
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html className="h-full antialiased" lang="en">
|
||||
<Head>
|
||||
<script dangerouslySetInnerHTML={{ __html: modeScript }} />
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/rss/feed.xml`}
|
||||
/>
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/feed+json"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/rss/feed.json`}
|
||||
/>
|
||||
</Head>
|
||||
<body className="flex h-full flex-col bg-zinc-50 dark:bg-black">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
123
src/pages/about.jsx
Normal file
@ -0,0 +1,123 @@
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Container } from '@/components/Container'
|
||||
import {
|
||||
GitHubIcon,
|
||||
InstagramIcon,
|
||||
LinkedInIcon,
|
||||
TwitterIcon,
|
||||
} from '@/components/SocialIcons'
|
||||
import portraitImage from '@/images/portrait.jpg'
|
||||
|
||||
function SocialLink({ className, href, children, icon: Icon }) {
|
||||
return (
|
||||
<li className={clsx(className, 'flex')}>
|
||||
<Link
|
||||
href={href}
|
||||
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
|
||||
>
|
||||
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
|
||||
<span className="ml-4">{children}</span>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function MailIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M6 5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6Zm.245 2.187a.75.75 0 0 0-.99 1.126l6.25 5.5a.75.75 0 0 0 .99 0l6.25-5.5a.75.75 0 0 0-.99-1.126L12 12.251 6.245 7.187Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>About - Spencer Sharp</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="I’m Spencer Sharp. I live in New York City, where I design the future."
|
||||
/>
|
||||
</Head>
|
||||
<Container className="mt-16 sm:mt-32">
|
||||
<div className="grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
|
||||
<div className="lg:pl-20">
|
||||
<div className="max-w-xs px-2.5 lg:max-w-none">
|
||||
<Image
|
||||
src={portraitImage}
|
||||
alt=""
|
||||
sizes="(min-width: 1024px) 32rem, 20rem"
|
||||
className="aspect-square rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:order-first lg:row-span-2">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
I’m 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>
|
||||
I’ve 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 sister’s motorcycle helmet, counted
|
||||
down from three, and jumped — hoping the tree was tall enough
|
||||
that with just a bit of momentum I’d 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 dad’s Blackberry into orbit and was able to transmit a
|
||||
photo back down to our family computer from space.
|
||||
</p>
|
||||
<p>
|
||||
Today, I’m the founder of Planetaria, where we’re 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,86 @@
|
||||
import { ArticleLayout } from '@/components/ArticleLayout'
|
||||
import Image from 'next/image'
|
||||
import designSystem from './planetaria-design-system.png'
|
||||
|
||||
export const meta = {
|
||||
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 default (props) => <ArticleLayout meta={meta} {...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.
|
After Width: | Height: | Size: 51 KiB |
69
src/pages/articles/index.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import Head from 'next/head'
|
||||
|
||||
import { Card } from '@/components/Card'
|
||||
import { SimpleLayout } from '@/components/SimpleLayout'
|
||||
import { formatDate } from '@/lib/formatDate'
|
||||
import { getAllArticles } from '@/lib/getAllArticles'
|
||||
|
||||
function Article({ article }) {
|
||||
return (
|
||||
<article className="md:grid md:grid-cols-4 md:items-baseline">
|
||||
<Card className="md:col-span-3">
|
||||
<Card.Title href={`/articles/${article.slug}`}>
|
||||
{article.title}
|
||||
</Card.Title>
|
||||
<Card.Eyebrow
|
||||
as="time"
|
||||
dateTime={article.date}
|
||||
className="md:hidden"
|
||||
decorate
|
||||
>
|
||||
{formatDate(article.date)}
|
||||
</Card.Eyebrow>
|
||||
<Card.Description>{article.description}</Card.Description>
|
||||
<Card.Cta>Read article</Card.Cta>
|
||||
</Card>
|
||||
<Card.Eyebrow
|
||||
as="time"
|
||||
dateTime={article.date}
|
||||
className="mt-1 hidden md:block"
|
||||
>
|
||||
{formatDate(article.date)}
|
||||
</Card.Eyebrow>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ArticlesIndex({ articles }) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Articles - Spencer Sharp</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order."
|
||||
/>
|
||||
</Head>
|
||||
<SimpleLayout
|
||||
title="Writing on 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
articles: (await getAllArticles()).map(({ component, ...meta }) => meta),
|
||||
},
|
||||
}
|
||||
}
|
101
src/pages/articles/introducing-animaginary.mdx
Normal file
@ -0,0 +1,101 @@
|
||||
import { ArticleLayout } from '@/components/ArticleLayout'
|
||||
|
||||
export const meta = {
|
||||
author: 'Adam Wathan',
|
||||
date: '2022-09-02',
|
||||
title: 'Introducing Animaginary: High performance web animations',
|
||||
description:
|
||||
'When you’re 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 default (props) => <ArticleLayout meta={meta} {...props} />
|
||||
|
||||
When you’re 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.
|
96
src/pages/articles/rewriting-the-cosmos-kernel-in-rust.mdx
Normal file
@ -0,0 +1,96 @@
|
||||
import { ArticleLayout } from '@/components/ArticleLayout'
|
||||
|
||||
export const meta = {
|
||||
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 it’s been a while since I’ve 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 default (props) => <ArticleLayout meta={meta} {...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 it’s been a while since I’ve 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.
|
326
src/pages/index.jsx
Normal file
@ -0,0 +1,326 @@
|
||||
import Head from 'next/head'
|
||||
import Image 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 { formatDate } from '@/lib/formatDate'
|
||||
import { generateRssFeed } from '@/lib/generateRssFeed'
|
||||
import { getAllArticles } from '@/lib/getAllArticles'
|
||||
|
||||
function MailIcon(props) {
|
||||
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) {
|
||||
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) {
|
||||
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 }) {
|
||||
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 }) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function Resume() {
|
||||
let resume = [
|
||||
{
|
||||
company: 'Planetaria',
|
||||
title: 'CEO',
|
||||
logo: logoPlanetaria,
|
||||
start: '2019',
|
||||
end: {
|
||||
label: 'Present',
|
||||
dateTime: new Date().getFullYear(),
|
||||
},
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<li key={roleIndex} 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={`${role.start.label ?? role.start} until ${
|
||||
role.end.label ?? role.end
|
||||
}`}
|
||||
>
|
||||
<time dateTime={role.start.dateTime ?? role.start}>
|
||||
{role.start.label ?? role.start}
|
||||
</time>{' '}
|
||||
<span aria-hidden="true">—</span>{' '}
|
||||
<time dateTime={role.end.dateTime ?? role.end}>
|
||||
{role.end.label ?? role.end}
|
||||
</time>
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
))}
|
||||
</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 function Home({ articles }) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
Spencer Sharp - Software designer, founder, and amateur astronaut
|
||||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="I’m Spencer, a software designer and entrepreneur based in New York City. I’m the founder and CEO of Planetaria, where we develop technologies that empower regular people to explore space on their own terms."
|
||||
/>
|
||||
</Head>
|
||||
<Container className="mt-9">
|
||||
<div className="max-w-2xl">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
Software designer, founder, and amateur astronaut.
|
||||
</h1>
|
||||
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
|
||||
I’m Spencer, a software designer and entrepreneur based in New York
|
||||
City. I’m 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
await generateRssFeed()
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
articles: (await getAllArticles())
|
||||
.slice(0, 4)
|
||||
.map(({ component, ...meta }) => meta),
|
||||
},
|
||||
}
|
||||
}
|
103
src/pages/projects.jsx
Normal file
@ -0,0 +1,103 @@
|
||||
import Head from 'next/head'
|
||||
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) {
|
||||
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 default function Projects() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Projects - Spencer Sharp</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Things I’ve made trying to put my dent in the universe."
|
||||
/>
|
||||
</Head>
|
||||
<SimpleLayout
|
||||
title="Things I’ve made trying to put my dent in the universe."
|
||||
intro="I’ve worked on tons of little projects over the years but these are the ones that I’m 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>
|
||||
</>
|
||||
)
|
||||
}
|
86
src/pages/speaking.jsx
Normal file
@ -0,0 +1,86 @@
|
||||
import Head from 'next/head'
|
||||
|
||||
import { Card } from '@/components/Card'
|
||||
import { Section } from '@/components/Section'
|
||||
import { SimpleLayout } from '@/components/SimpleLayout'
|
||||
|
||||
function SpeakingSection({ children, ...props }) {
|
||||
return (
|
||||
<Section {...props}>
|
||||
<div className="space-y-16">{children}</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function Appearance({ title, description, event, cta, href }) {
|
||||
return (
|
||||
<Card as="article">
|
||||
<Card.Title as="h3" href={href}>
|
||||
{title}
|
||||
</Card.Title>
|
||||
<Card.Eyebrow decorate>{event}</Card.Eyebrow>
|
||||
<Card.Description>{description}</Card.Description>
|
||||
<Card.Cta>{cta}</Card.Cta>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Speaking() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Speaking - Spencer Sharp</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="I’ve spoken at events all around the world and been interviewed for many podcasts."
|
||||
/>
|
||||
</Head>
|
||||
<SimpleLayout
|
||||
title="I’ve spoken at events all around the world and been interviewed for many podcasts."
|
||||
intro="One of my favorite ways to share my ideas is live on stage, where there’s so much more communication bandwidth than there is in writing, and I love podcast interviews because they give me the opportunity to answer questions instead of just present my opinions."
|
||||
>
|
||||
<div className="space-y-20">
|
||||
<SpeakingSection title="Conferences">
|
||||
<Appearance
|
||||
href="#"
|
||||
title="In space, no one can watch you stream — until now"
|
||||
description="A technical deep-dive into HelioStream, the real-time streaming library I wrote for transmitting live video back to Earth."
|
||||
event="SysConf 2021"
|
||||
cta="Watch video"
|
||||
/>
|
||||
<Appearance
|
||||
href="#"
|
||||
title="Lessons learned from our first product recall"
|
||||
description="They say that if you’re not embarassed by your first version, you’re doing it wrong. Well when you’re selling DIY space shuttle kits it turns out it’s a bit more complicated."
|
||||
event="Business of Startups 2020"
|
||||
cta="Watch video"
|
||||
/>
|
||||
</SpeakingSection>
|
||||
<SpeakingSection title="Podcasts">
|
||||
<Appearance
|
||||
href="#"
|
||||
title="Using design as a competitive advantage"
|
||||
description="How we used world-class visual design to attract a great team, win over customers, and get more press for Planetaria."
|
||||
event="Encoding Design, July 2022"
|
||||
cta="Listen to podcast"
|
||||
/>
|
||||
<Appearance
|
||||
href="#"
|
||||
title="Bootstrapping an aerospace company to $17M ARR"
|
||||
description="The story of how we built one of the most promising space startups in the world without taking any capital from investors."
|
||||
event="The Escape Velocity Show, March 2022"
|
||||
cta="Listen to podcast"
|
||||
/>
|
||||
<Appearance
|
||||
href="#"
|
||||
title="Programming your company operating system"
|
||||
description="On the importance of creating systems and processes for running your business so that everyone on the team knows how to make the right decision no matter the situation."
|
||||
event="How They Work Radio, September 2021"
|
||||
cta="Listen to podcast"
|
||||
/>
|
||||
</SpeakingSection>
|
||||
</div>
|
||||
</SimpleLayout>
|
||||
</>
|
||||
)
|
||||
}
|
21
src/pages/thank-you.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
import Head from 'next/head'
|
||||
|
||||
import { SimpleLayout } from '@/components/SimpleLayout'
|
||||
|
||||
export default function ThankYou() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>You’re subscribed - Spencer Sharp</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Thanks for subscribing to my newsletter."
|
||||
/>
|
||||
</Head>
|
||||
<SimpleLayout
|
||||
title="Thanks for subscribing."
|
||||
intro="I’ll 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 you’d want to hear about. You can unsubscribe at any time, no hard feelings."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
119
src/pages/uses.jsx
Normal file
@ -0,0 +1,119 @@
|
||||
import Head from 'next/head'
|
||||
|
||||
import { Card } from '@/components/Card'
|
||||
import { Section } from '@/components/Section'
|
||||
import { SimpleLayout } from '@/components/SimpleLayout'
|
||||
|
||||
function ToolsSection({ children, ...props }) {
|
||||
return (
|
||||
<Section {...props}>
|
||||
<ul role="list" className="space-y-16">
|
||||
{children}
|
||||
</ul>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function Tool({ title, href, children }) {
|
||||
return (
|
||||
<Card as="li">
|
||||
<Card.Title as="h3" href={href}>
|
||||
{title}
|
||||
</Card.Title>
|
||||
<Card.Description>{children}</Card.Description>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Uses() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Uses - Spencer Sharp</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Software I use, gadgets I love, and other things I recommend."
|
||||
/>
|
||||
</Head>
|
||||
<SimpleLayout
|
||||
title="Software I use, gadgets I love, and other things I recommend."
|
||||
intro="I get asked a lot about the things I use to build software, stay productive, or buy to fool myself into thinking I’m being productive when I’m really just procrastinating. Here’s a big list of all of my favorite stuff."
|
||||
>
|
||||
<div className="space-y-20">
|
||||
<ToolsSection title="Workstation">
|
||||
<Tool title="16” MacBook Pro, M1 Max, 64GB RAM (2021)">
|
||||
I was using an Intel-based 16” MacBook Pro prior to this and the
|
||||
difference is night and day. I’ve never heard the fans turn on a
|
||||
single time, even under the incredibly heavy loads I put it
|
||||
through with our various launch simulations.
|
||||
</Tool>
|
||||
<Tool title="Apple Pro Display XDR (Standard Glass)">
|
||||
The only display on the market if you want something HiDPI and
|
||||
bigger than 27”. When you’re working at planetary scale, every
|
||||
pixel you can get counts.
|
||||
</Tool>
|
||||
<Tool title="IBM Model M SSK Industrial Keyboard">
|
||||
They don’t make keyboards the way they used to. I buy these any
|
||||
time I see them go up for sale and keep them in storage in case I
|
||||
need parts or need to retire my main.
|
||||
</Tool>
|
||||
<Tool title="Apple Magic Trackpad">
|
||||
Something about all the gestures makes me feel like a wizard with
|
||||
special powers. I really like feeling like a wizard with special
|
||||
powers.
|
||||
</Tool>
|
||||
<Tool title="Herman Miller Aeron Chair">
|
||||
If I’m going to slouch in the worst ergonomic position imaginable
|
||||
all day, I might as well do it in an expensive chair.
|
||||
</Tool>
|
||||
</ToolsSection>
|
||||
<ToolsSection title="Development tools">
|
||||
<Tool title="Sublime Text 4">
|
||||
I don’t care if it’s missing all of the fancy IDE features
|
||||
everyone else relies on, Sublime Text is still the best text
|
||||
editor ever made.
|
||||
</Tool>
|
||||
<Tool title="iTerm2">
|
||||
I’m honestly not even sure what features I get with this that
|
||||
aren’t just part of the macOS Terminal but it’s what I use.
|
||||
</Tool>
|
||||
<Tool title="TablePlus">
|
||||
Great software for working with databases. Has saved me from
|
||||
building about a thousand admin interfaces for my various projects
|
||||
over the years.
|
||||
</Tool>
|
||||
</ToolsSection>
|
||||
<ToolsSection title="Design">
|
||||
<Tool title="Figma">
|
||||
We started using Figma as just a design tool but now it’s become
|
||||
our virtual whiteboard for the entire company. Never would have
|
||||
expected the collaboration features to be the real hook.
|
||||
</Tool>
|
||||
</ToolsSection>
|
||||
<ToolsSection title="Productivity">
|
||||
<Tool title="Alfred">
|
||||
It’s not the newest kid on the block but it’s still the fastest.
|
||||
The Sublime Text of the application launcher world.
|
||||
</Tool>
|
||||
<Tool title="Reflect">
|
||||
Using a daily notes system instead of trying to keep things
|
||||
organized by topics has been super powerful for me. And with
|
||||
Reflect, it’s still easy for me to keep all of that stuff
|
||||
discoverable by topic even though all of my writing happens in the
|
||||
daily note.
|
||||
</Tool>
|
||||
<Tool title="SavvyCal">
|
||||
Great tool for scheduling meetings while protecting my calendar
|
||||
and making sure I still have lots of time for deep work during the
|
||||
week.
|
||||
</Tool>
|
||||
<Tool title="Focus">
|
||||
Simple tool for blocking distracting websites when I need to just
|
||||
do the work and get some momentum going.
|
||||
</Tool>
|
||||
</ToolsSection>
|
||||
</div>
|
||||
</SimpleLayout>
|
||||
</>
|
||||
)
|
||||
}
|
47
src/styles/prism.css
Normal file
@ -0,0 +1,47 @@
|
||||
pre[class*='language-'] {
|
||||
color: theme('colors.zinc.100');
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.class-name,
|
||||
.token.selector,
|
||||
.token.selector .class,
|
||||
.token.selector.class,
|
||||
.token.function {
|
||||
color: theme('colors.pink.400');
|
||||
}
|
||||
|
||||
.token.attr-name,
|
||||
.token.keyword,
|
||||
.token.rule,
|
||||
.token.pseudo-class,
|
||||
.token.important {
|
||||
color: theme('colors.zinc.300');
|
||||
}
|
||||
|
||||
.token.module {
|
||||
color: theme('colors.pink.400');
|
||||
}
|
||||
|
||||
.token.attr-value,
|
||||
.token.class,
|
||||
.token.string,
|
||||
.token.property {
|
||||
color: theme('colors.teal.300');
|
||||
}
|
||||
|
||||
.token.punctuation,
|
||||
.token.attr-equals {
|
||||
color: theme('colors.zinc.500');
|
||||
}
|
||||
|
||||
.token.unit,
|
||||
.language-css .token.function {
|
||||
color: theme('colors.sky.200');
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.operator,
|
||||
.token.combinator {
|
||||
color: theme('colors.zinc.400');
|
||||
}
|
4
src/styles/tailwind.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import './prism.css';
|
||||
@import 'tailwindcss/utilities';
|
303
tailwind.config.js
Normal file
@ -0,0 +1,303 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx}'],
|
||||
darkMode: 'class',
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
theme: {
|
||||
fontSize: {
|
||||
xs: ['0.8125rem', { lineHeight: '1.5rem' }],
|
||||
sm: ['0.875rem', { lineHeight: '1.5rem' }],
|
||||
base: ['1rem', { lineHeight: '1.75rem' }],
|
||||
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
xl: ['1.25rem', { lineHeight: '2rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||
'4xl': ['2rem', { lineHeight: '2.5rem' }],
|
||||
'5xl': ['3rem', { lineHeight: '3.5rem' }],
|
||||
'6xl': ['3.75rem', { lineHeight: '1' }],
|
||||
'7xl': ['4.5rem', { lineHeight: '1' }],
|
||||
'8xl': ['6rem', { lineHeight: '1' }],
|
||||
'9xl': ['8rem', { lineHeight: '1' }],
|
||||
},
|
||||
typography: (theme) => ({
|
||||
invert: {
|
||||
css: {
|
||||
'--tw-prose-body': 'var(--tw-prose-invert-body)',
|
||||
'--tw-prose-headings': 'var(--tw-prose-invert-headings)',
|
||||
'--tw-prose-links': 'var(--tw-prose-invert-links)',
|
||||
'--tw-prose-links-hover': 'var(--tw-prose-invert-links-hover)',
|
||||
'--tw-prose-underline': 'var(--tw-prose-invert-underline)',
|
||||
'--tw-prose-underline-hover':
|
||||
'var(--tw-prose-invert-underline-hover)',
|
||||
'--tw-prose-bold': 'var(--tw-prose-invert-bold)',
|
||||
'--tw-prose-counters': 'var(--tw-prose-invert-counters)',
|
||||
'--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
|
||||
'--tw-prose-hr': 'var(--tw-prose-invert-hr)',
|
||||
'--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
|
||||
'--tw-prose-captions': 'var(--tw-prose-invert-captions)',
|
||||
'--tw-prose-code': 'var(--tw-prose-invert-code)',
|
||||
'--tw-prose-code-bg': 'var(--tw-prose-invert-code-bg)',
|
||||
'--tw-prose-pre-code': 'var(--tw-prose-invert-pre-code)',
|
||||
'--tw-prose-pre-bg': 'var(--tw-prose-invert-pre-bg)',
|
||||
'--tw-prose-pre-border': 'var(--tw-prose-invert-pre-border)',
|
||||
'--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
|
||||
'--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
|
||||
},
|
||||
},
|
||||
DEFAULT: {
|
||||
css: {
|
||||
'--tw-prose-body': theme('colors.zinc.600'),
|
||||
'--tw-prose-headings': theme('colors.zinc.900'),
|
||||
'--tw-prose-links': theme('colors.teal.500'),
|
||||
'--tw-prose-links-hover': theme('colors.teal.600'),
|
||||
'--tw-prose-underline': theme('colors.teal.500 / 0.2'),
|
||||
'--tw-prose-underline-hover': theme('colors.teal.500'),
|
||||
'--tw-prose-bold': theme('colors.zinc.900'),
|
||||
'--tw-prose-counters': theme('colors.zinc.900'),
|
||||
'--tw-prose-bullets': theme('colors.zinc.900'),
|
||||
'--tw-prose-hr': theme('colors.zinc.100'),
|
||||
'--tw-prose-quote-borders': theme('colors.zinc.200'),
|
||||
'--tw-prose-captions': theme('colors.zinc.400'),
|
||||
'--tw-prose-code': theme('colors.zinc.700'),
|
||||
'--tw-prose-code-bg': theme('colors.zinc.300 / 0.2'),
|
||||
'--tw-prose-pre-code': theme('colors.zinc.100'),
|
||||
'--tw-prose-pre-bg': theme('colors.zinc.900'),
|
||||
'--tw-prose-pre-border': 'transparent',
|
||||
'--tw-prose-th-borders': theme('colors.zinc.200'),
|
||||
'--tw-prose-td-borders': theme('colors.zinc.100'),
|
||||
|
||||
'--tw-prose-invert-body': theme('colors.zinc.400'),
|
||||
'--tw-prose-invert-headings': theme('colors.zinc.200'),
|
||||
'--tw-prose-invert-links': theme('colors.teal.400'),
|
||||
'--tw-prose-invert-links-hover': theme('colors.teal.400'),
|
||||
'--tw-prose-invert-underline': theme('colors.teal.400 / 0.3'),
|
||||
'--tw-prose-invert-underline-hover': theme('colors.teal.400'),
|
||||
'--tw-prose-invert-bold': theme('colors.zinc.200'),
|
||||
'--tw-prose-invert-counters': theme('colors.zinc.200'),
|
||||
'--tw-prose-invert-bullets': theme('colors.zinc.200'),
|
||||
'--tw-prose-invert-hr': theme('colors.zinc.700 / 0.4'),
|
||||
'--tw-prose-invert-quote-borders': theme('colors.zinc.500'),
|
||||
'--tw-prose-invert-captions': theme('colors.zinc.500'),
|
||||
'--tw-prose-invert-code': theme('colors.zinc.300'),
|
||||
'--tw-prose-invert-code-bg': theme('colors.zinc.200 / 0.05'),
|
||||
'--tw-prose-invert-pre-code': theme('colors.zinc.100'),
|
||||
'--tw-prose-invert-pre-bg': 'rgb(0 0 0 / 0.4)',
|
||||
'--tw-prose-invert-pre-border': theme('colors.zinc.200 / 0.1'),
|
||||
'--tw-prose-invert-th-borders': theme('colors.zinc.700'),
|
||||
'--tw-prose-invert-td-borders': theme('colors.zinc.800'),
|
||||
|
||||
// Base
|
||||
color: 'var(--tw-prose-body)',
|
||||
lineHeight: theme('lineHeight.7'),
|
||||
'> *': {
|
||||
marginTop: theme('spacing.10'),
|
||||
marginBottom: theme('spacing.10'),
|
||||
},
|
||||
p: {
|
||||
marginTop: theme('spacing.7'),
|
||||
marginBottom: theme('spacing.7'),
|
||||
},
|
||||
|
||||
// Headings
|
||||
'h2, h3': {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
},
|
||||
h2: {
|
||||
fontSize: theme('fontSize.xl')[0],
|
||||
lineHeight: theme('lineHeight.7'),
|
||||
marginTop: theme('spacing.20'),
|
||||
marginBottom: theme('spacing.4'),
|
||||
},
|
||||
h3: {
|
||||
fontSize: theme('fontSize.base')[0],
|
||||
lineHeight: theme('lineHeight.7'),
|
||||
marginTop: theme('spacing.16'),
|
||||
marginBottom: theme('spacing.4'),
|
||||
},
|
||||
':is(h2, h3) + *': {
|
||||
marginTop: 0,
|
||||
},
|
||||
|
||||
// Images
|
||||
img: {
|
||||
borderRadius: theme('borderRadius.3xl'),
|
||||
},
|
||||
|
||||
// Inline elements
|
||||
a: {
|
||||
color: 'var(--tw-prose-links)',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
textDecoration: 'underline',
|
||||
textDecorationColor: 'var(--tw-prose-underline)',
|
||||
transitionProperty: 'color, text-decoration-color',
|
||||
transitionDuration: theme('transitionDuration.150'),
|
||||
transitionTimingFunction: theme('transitionTimingFunction.in-out'),
|
||||
},
|
||||
'a:hover': {
|
||||
color: 'var(--tw-prose-links-hover)',
|
||||
textDecorationColor: 'var(--tw-prose-underline-hover)',
|
||||
},
|
||||
strong: {
|
||||
color: 'var(--tw-prose-bold)',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
},
|
||||
code: {
|
||||
display: 'inline-block',
|
||||
color: 'var(--tw-prose-code)',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
backgroundColor: 'var(--tw-prose-code-bg)',
|
||||
borderRadius: theme('borderRadius.lg'),
|
||||
paddingLeft: theme('spacing.1'),
|
||||
paddingRight: theme('spacing.1'),
|
||||
},
|
||||
'a code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
':is(h2, h3) code': {
|
||||
fontWeight: theme('fontWeight.bold'),
|
||||
},
|
||||
|
||||
// Quotes
|
||||
blockquote: {
|
||||
paddingLeft: theme('spacing.6'),
|
||||
borderLeftWidth: theme('borderWidth.2'),
|
||||
borderLeftColor: 'var(--tw-prose-quote-borders)',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
|
||||
// Figures
|
||||
figcaption: {
|
||||
color: 'var(--tw-prose-captions)',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
lineHeight: theme('lineHeight.6'),
|
||||
marginTop: theme('spacing.3'),
|
||||
},
|
||||
'figcaption > p': {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
// Lists
|
||||
ul: {
|
||||
listStyleType: 'disc',
|
||||
},
|
||||
ol: {
|
||||
listStyleType: 'decimal',
|
||||
},
|
||||
'ul, ol': {
|
||||
paddingLeft: theme('spacing.6'),
|
||||
},
|
||||
li: {
|
||||
marginTop: theme('spacing.6'),
|
||||
marginBottom: theme('spacing.6'),
|
||||
paddingLeft: theme('spacing[3.5]'),
|
||||
},
|
||||
'li::marker': {
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
},
|
||||
'ol > li::marker': {
|
||||
color: 'var(--tw-prose-counters)',
|
||||
},
|
||||
'ul > li::marker': {
|
||||
color: 'var(--tw-prose-bullets)',
|
||||
},
|
||||
'li :is(ol, ul)': {
|
||||
marginTop: theme('spacing.4'),
|
||||
marginBottom: theme('spacing.4'),
|
||||
},
|
||||
'li :is(li, p)': {
|
||||
marginTop: theme('spacing.3'),
|
||||
marginBottom: theme('spacing.3'),
|
||||
},
|
||||
|
||||
// Code blocks
|
||||
pre: {
|
||||
color: 'var(--tw-prose-pre-code)',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
fontWeight: theme('fontWeight.medium'),
|
||||
backgroundColor: 'var(--tw-prose-pre-bg)',
|
||||
borderRadius: theme('borderRadius.3xl'),
|
||||
padding: theme('spacing.8'),
|
||||
overflowX: 'auto',
|
||||
border: '1px solid',
|
||||
borderColor: 'var(--tw-prose-pre-border)',
|
||||
},
|
||||
'pre code': {
|
||||
display: 'inline',
|
||||
color: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
fontWeight: 'inherit',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
padding: 0,
|
||||
},
|
||||
|
||||
// Horizontal rules
|
||||
hr: {
|
||||
marginTop: theme('spacing.20'),
|
||||
marginBottom: theme('spacing.20'),
|
||||
borderTopWidth: '1px',
|
||||
borderColor: 'var(--tw-prose-hr)',
|
||||
'@screen lg': {
|
||||
marginLeft: `calc(${theme('spacing.12')} * -1)`,
|
||||
marginRight: `calc(${theme('spacing.12')} * -1)`,
|
||||
},
|
||||
},
|
||||
|
||||
// Tables
|
||||
table: {
|
||||
width: '100%',
|
||||
tableLayout: 'auto',
|
||||
textAlign: 'left',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
},
|
||||
thead: {
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'var(--tw-prose-th-borders)',
|
||||
},
|
||||
'thead th': {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
verticalAlign: 'bottom',
|
||||
paddingBottom: theme('spacing.2'),
|
||||
},
|
||||
'thead th:not(:first-child)': {
|
||||
paddingLeft: theme('spacing.2'),
|
||||
},
|
||||
'thead th:not(:last-child)': {
|
||||
paddingRight: theme('spacing.2'),
|
||||
},
|
||||
'tbody tr': {
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'var(--tw-prose-td-borders)',
|
||||
},
|
||||
'tbody tr:last-child': {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
'tbody td': {
|
||||
verticalAlign: 'baseline',
|
||||
},
|
||||
tfoot: {
|
||||
borderTopWidth: '1px',
|
||||
borderTopColor: 'var(--tw-prose-th-borders)',
|
||||
},
|
||||
'tfoot td': {
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
':is(tbody, tfoot) td': {
|
||||
paddingTop: theme('spacing.2'),
|
||||
paddingBottom: theme('spacing.2'),
|
||||
},
|
||||
':is(tbody, tfoot) td:not(:first-child)': {
|
||||
paddingLeft: theme('spacing.2'),
|
||||
},
|
||||
':is(tbody, tfoot) td:not(:last-child)': {
|
||||
paddingRight: theme('spacing.2'),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|