mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 09:25:22 -04:00
CSS modules ➡️ Stitches 🧵 (#799)
This commit is contained in:
parent
ac7ac71c10
commit
c2dde042b7
@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": ["stylelint-prettier/recommended"],
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"rules": {
|
||||
"alpha-value-notation": "number",
|
||||
"color-function-notation": "legacy",
|
||||
"color-hex-length": "long",
|
||||
"no-descending-specificity": null,
|
||||
"rule-empty-line-before": null,
|
||||
"selector-class-pattern": null
|
||||
}
|
||||
}
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -1,12 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"divlo.vscode-styled-jsx-languageserver",
|
||||
"divlo.vscode-styled-jsx-syntax",
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"silvenon.mdx",
|
||||
"stylelint.vscode-stylelint",
|
||||
"wix.vscode-import-cost"
|
||||
]
|
||||
}
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -11,9 +11,6 @@
|
||||
},
|
||||
"prettier.requireConfig": true,
|
||||
"prettier.configPath": ".prettierrc.json",
|
||||
"stylelint.packageManager": "yarn",
|
||||
"stylelint.reportNeedlessDisables": true,
|
||||
"stylelint.reportInvalidScopeDisables": true,
|
||||
"npm.packageManager": "yarn",
|
||||
"eslint.packageManager": "yarn"
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
[](https://github.com/jakejarvis/jarv.is)
|
||||
[](http://jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion/)
|
||||
|
||||
Personal website of [@jakejarvis](https://github.com/jakejarvis), created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [and more](https://jarv.is/humans.txt).
|
||||
Personal website of [@jakejarvis](https://github.com/jakejarvis), created and deployed using [Next.js](https://nextjs.org/), [Stitches](https://stitches.dev/), [Vercel](https://vercel.com/), [and more](https://jarv.is/humans.txt).
|
||||
|
||||
I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too!
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
.blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1.25em;
|
||||
border-left: 0.25em solid var(--link);
|
||||
color: var(--medium-dark);
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
import classNames from "classnames";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
|
||||
import styles from "./Blockquote.module.css";
|
||||
|
||||
export type BlockquoteProps = JSX.IntrinsicElements["blockquote"];
|
||||
|
||||
const Blockquote = ({ className, ...rest }: BlockquoteProps) => (
|
||||
<blockquote className={classNames(styles.blockquote, className)} {...rest} />
|
||||
);
|
||||
const Blockquote = styled("blockquote", {
|
||||
marginLeft: 0,
|
||||
paddingLeft: "1.25em",
|
||||
borderLeft: "0.25em solid $link",
|
||||
color: "$mediumDark",
|
||||
});
|
||||
|
||||
export default Blockquote;
|
||||
|
@ -7,6 +7,7 @@ export type CaptchaProps = {
|
||||
size?: "normal" | "compact" | "invisible";
|
||||
theme?: "light" | "dark";
|
||||
id?: string;
|
||||
className?: string;
|
||||
|
||||
// callbacks pulled verbatim from node_modules/@hcaptcha/react-hcaptcha/types/index.d.ts
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
@ -20,7 +21,7 @@ export type CaptchaProps = {
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
};
|
||||
|
||||
const Captcha = ({ size = "normal", theme, id, ...rest }: CaptchaProps) => {
|
||||
const Captcha = ({ size = "normal", theme, id, className, ...rest }: CaptchaProps) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
@ -30,15 +31,17 @@ const Captcha = ({ size = "normal", theme, id, ...rest }: CaptchaProps) => {
|
||||
<link rel="preconnect" href="https://newassets.hcaptcha.com" />
|
||||
</Head>
|
||||
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
|
||||
reCaptchaCompat={false}
|
||||
tabIndex={0}
|
||||
size={size}
|
||||
theme={theme || (resolvedTheme === "dark" ? "dark" : "light")}
|
||||
id={id}
|
||||
{...rest}
|
||||
/>
|
||||
<div className={className}>
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
|
||||
reCaptchaCompat={false}
|
||||
tabIndex={0}
|
||||
size={size}
|
||||
theme={theme || (resolvedTheme === "dark" ? "dark" : "light")}
|
||||
id={id}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,133 +0,0 @@
|
||||
/* all code */
|
||||
.code {
|
||||
font-size: 0.925em;
|
||||
color: var(--code-text);
|
||||
page-break-inside: avoid;
|
||||
background-color: var(--code-background);
|
||||
border: 1px solid var(--kinda-light);
|
||||
border-radius: var(--rounded-edge-radius);
|
||||
|
||||
/* light-dark theme switch fading */
|
||||
transition: background 0.25s ease, border 0.25s ease;
|
||||
}
|
||||
|
||||
/* inline code in paragraphs/elsewhere (single backticks) */
|
||||
.inline {
|
||||
padding: 0.2em 0.3em;
|
||||
}
|
||||
|
||||
/* full-blown code blocks, with copy/paste button and (usually) line numbers */
|
||||
.block {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.block .code {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.copy_btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.65em;
|
||||
color: var(--medium-dark);
|
||||
background-color: var(--background-inner);
|
||||
border: 1px solid var(--kinda-light);
|
||||
border-top-right-radius: var(--rounded-edge-radius);
|
||||
border-end-start-radius: var(--rounded-edge-radius);
|
||||
|
||||
/* light-dark theme switch fading */
|
||||
transition: background 0.25s ease, border 0.25s ease;
|
||||
}
|
||||
|
||||
.copy_btn:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
/* the following sub-classes MUST be global -- the prism rehype plugin isn't aware of this file */
|
||||
|
||||
/* leave room for clipboard button to the right of the first line */
|
||||
.highlight > :global(.code-line:first-of-type) {
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
.highlight > :global(.line-number::before) {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
text-align: right;
|
||||
color: var(--code-comment);
|
||||
content: attr(line); /* added to spans by prism */
|
||||
}
|
||||
|
||||
.highlight :global(.token.comment),
|
||||
.highlight :global(.token.prolog),
|
||||
.highlight :global(.token.cdata) {
|
||||
color: var(--code-comment);
|
||||
}
|
||||
|
||||
.highlight :global(.token.delimiter),
|
||||
.highlight :global(.token.boolean),
|
||||
.highlight :global(.token.keyword),
|
||||
.highlight :global(.token.selector),
|
||||
.highlight :global(.token.important),
|
||||
.highlight :global(.token.doctype),
|
||||
.highlight :global(.token.atrule),
|
||||
.highlight :global(.token.url) {
|
||||
color: var(--code-keyword);
|
||||
}
|
||||
|
||||
.highlight :global(.token.tag),
|
||||
.highlight :global(.token.builtin),
|
||||
.highlight :global(.token.regex) {
|
||||
color: var(--code-namespace);
|
||||
}
|
||||
|
||||
.highlight :global(.token.property),
|
||||
.highlight :global(.token.constant),
|
||||
.highlight :global(.token.variable),
|
||||
.highlight :global(.token.attr-value),
|
||||
.highlight :global(.token.class-name),
|
||||
.highlight :global(.token.string),
|
||||
.highlight :global(.token.char) {
|
||||
color: var(--code-variable);
|
||||
}
|
||||
|
||||
.highlight :global(.token.literal-property),
|
||||
.highlight :global(.token.attr-name) {
|
||||
color: var(--code-attribute);
|
||||
}
|
||||
|
||||
.highlight :global(.token.function) {
|
||||
color: var(--code-literal);
|
||||
}
|
||||
|
||||
.highlight :global(.token.tag .punctuation),
|
||||
.highlight :global(.token.attr-value .punctuation) {
|
||||
color: var(--code-punctuation);
|
||||
}
|
||||
|
||||
.highlight :global(.token.inserted) {
|
||||
background-color: var(--code-addition);
|
||||
}
|
||||
|
||||
.highlight :global(.token.deleted) {
|
||||
background-color: var(--code-deletion);
|
||||
}
|
||||
|
||||
.highlight :global(.token.url) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.highlight :global(.token.bold) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlight :global(.token.italic) {
|
||||
font-style: italic;
|
||||
}
|
@ -1,9 +1,109 @@
|
||||
import classNames from "classnames";
|
||||
import CopyButton from "../CopyButton/CopyButton";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./CodeBlock.module.css";
|
||||
const Code = styled("code", {
|
||||
fontSize: "0.925em",
|
||||
color: "$codeText",
|
||||
pageBreakInside: "avoid",
|
||||
backgroundColor: "$codeBackground",
|
||||
border: "1px solid $kindaLight",
|
||||
borderRadius: "$rounded",
|
||||
|
||||
export type CodeBlockProps = JSX.IntrinsicElements["code"] & {
|
||||
// light-dark theme switch fading
|
||||
transition: "background 0.25s ease, border 0.25s ease",
|
||||
|
||||
variants: {
|
||||
highlight: {
|
||||
// the following sub-classes MUST be global -- the prism rehype plugin isn't aware of this file
|
||||
true: {
|
||||
// leave room for clipboard button to the right of the first line
|
||||
".code-line:first-of-type": {
|
||||
marginRight: "3em",
|
||||
},
|
||||
".line-number::before": {
|
||||
display: "inline-block",
|
||||
width: "1.5em",
|
||||
marginRight: "1.5em",
|
||||
textAlign: "right",
|
||||
color: "$codeComment",
|
||||
content: "attr(line)", // added to spans by prism
|
||||
userSelect: "none",
|
||||
},
|
||||
".token": {
|
||||
"&.comment, &.prolog, &.cdata": {
|
||||
color: "$codeComment",
|
||||
},
|
||||
"&.delimiter, &.boolean, &.keyword, &.selector, &.important, &.doctype, &.atrule, &.url": {
|
||||
color: "$codeKeyword",
|
||||
},
|
||||
"&.tag, &.builtin, &.regex": {
|
||||
color: "$codeNamespace",
|
||||
},
|
||||
"&.property, &.constant, &.variable, &.attr-value, &.class-name, &.string, &.char": {
|
||||
color: "$codeVariable",
|
||||
},
|
||||
"&.literal-property, &.attr-name": {
|
||||
color: "$codeAttribute",
|
||||
},
|
||||
"&.function": {
|
||||
color: "$codeLiteral",
|
||||
},
|
||||
"&.tag .punctuation, &.attr-value .punctuation": {
|
||||
color: "$codePunctuation",
|
||||
},
|
||||
"&.inserted": {
|
||||
color: "$codeAddition",
|
||||
},
|
||||
"&.deleted": {
|
||||
color: "$codeDeletion",
|
||||
},
|
||||
"&.url": { textDecoration: "underline" },
|
||||
"&.bold": { fontWeight: "bold" },
|
||||
"&.italic": { fontStyle: "italic" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const InlineCode = styled(Code, {
|
||||
padding: "0.2em 0.3em",
|
||||
});
|
||||
|
||||
const Block = styled("div", {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
margin: "1em auto",
|
||||
|
||||
[`& ${Code}`]: {
|
||||
display: "block",
|
||||
overflowX: "auto",
|
||||
padding: "1em",
|
||||
tabSize: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const CornerCopyButton = styled(CopyButton, {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
padding: "0.65em",
|
||||
color: "$mediumDark",
|
||||
backgroundColor: "$backgroundInner",
|
||||
border: "1px solid $kindaLight",
|
||||
borderTopRightRadius: "$radii$rounded",
|
||||
borderEndStartRadius: "$radii$rounded",
|
||||
|
||||
// light-dark theme switch fading
|
||||
transition: "background 0.25s ease, border 0.25s ease",
|
||||
|
||||
"&:hover": {
|
||||
color: "$link",
|
||||
},
|
||||
});
|
||||
|
||||
export type CodeBlockProps = ComponentProps<typeof Code> & {
|
||||
forceBlock?: boolean;
|
||||
};
|
||||
|
||||
@ -15,26 +115,19 @@ const CodeBlock = ({ forceBlock, className, children, ...rest }: CodeBlockProps)
|
||||
// full multi-line code blocks with copy-to-clipboard button
|
||||
// automatic if highlighted by prism, otherwise can be forced (rather than inlined) with `forceBlock={true}`
|
||||
return (
|
||||
<div className={styles.block}>
|
||||
<CopyButton source={children} className={styles.copy_btn} />
|
||||
<code
|
||||
className={classNames(
|
||||
styles.code,
|
||||
prismEnabled && styles.highlight,
|
||||
className?.replace("code-highlight", "").trim()
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<Block>
|
||||
<CornerCopyButton source={children} />
|
||||
<Code highlight={prismEnabled} className={className?.replace("code-highlight", "").trim()} {...rest}>
|
||||
{children}
|
||||
</code>
|
||||
</div>
|
||||
</Code>
|
||||
</Block>
|
||||
);
|
||||
} else {
|
||||
// inline code in paragraphs, headings, etc. (not highlighted)
|
||||
return (
|
||||
<code className={classNames(styles.code, styles.inline, className)} {...rest}>
|
||||
<InlineCode className={className} {...rest}>
|
||||
{children}
|
||||
</code>
|
||||
</InlineCode>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { memo } from "react";
|
||||
import css from "styled-jsx/css";
|
||||
import classNames from "classnames";
|
||||
import Link, { CustomLinkProps } from "../Link/Link";
|
||||
|
||||
export type ColorfulLinkProps = CustomLinkProps & {
|
||||
lightColor: string;
|
||||
darkColor: string;
|
||||
};
|
||||
|
||||
// spits out alpha'd version of given color in rgba() format within a linear-gradient (that's not really a gradient)
|
||||
const getLinearGradient = (hex: string, alpha = 0.4) => {
|
||||
// hex -> rgb, pulled from https://github.com/sindresorhus/hex-rgb/blob/main/index.js#L29
|
||||
const number = Number.parseInt(hex.replace(/^#/, ""), 16);
|
||||
const red = number >> 16;
|
||||
const green = (number >> 8) & 255;
|
||||
const blue = number & 255;
|
||||
|
||||
const rgbaString = `rgba(${red},${green},${blue},${alpha})`;
|
||||
|
||||
return `linear-gradient(${rgbaString},${rgbaString})`;
|
||||
};
|
||||
|
||||
const ColorfulLink = ({ lightColor, darkColor, className, ...rest }: ColorfulLinkProps) => {
|
||||
const { className: underlineClassName, styles: underlineStyles } = css.resolve`
|
||||
a {
|
||||
color: ${lightColor};
|
||||
background-image: ${getLinearGradient(lightColor)};
|
||||
}
|
||||
:global([data-theme="dark"]) a {
|
||||
color: ${darkColor};
|
||||
background-image: ${getLinearGradient(darkColor)};
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link className={classNames(underlineClassName, className)} {...rest} />
|
||||
|
||||
{underlineStyles}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ColorfulLink);
|
@ -1,6 +0,0 @@
|
||||
.wrapper :global(.giscus) {
|
||||
margin-top: 2em;
|
||||
padding-top: 2em;
|
||||
border-top: 2px solid var(--light);
|
||||
min-height: 350px;
|
||||
}
|
@ -1,21 +1,29 @@
|
||||
import { memo } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import classNames from "classnames";
|
||||
import { Giscus } from "@giscus/react";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import { giscusConfig } from "../../lib/config";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { GiscusProps } from "@giscus/react";
|
||||
|
||||
import styles from "./Comments.module.css";
|
||||
const Wrapper = styled("div", {
|
||||
".giscus": {
|
||||
marginTop: "2em",
|
||||
paddingTop: "2em",
|
||||
borderTop: "2px solid $light",
|
||||
minHeight: "350px",
|
||||
},
|
||||
});
|
||||
|
||||
export type CommentsProps = JSX.IntrinsicElements["div"] & {
|
||||
export type CommentsProps = ComponentProps<typeof Wrapper> & {
|
||||
title: string;
|
||||
};
|
||||
|
||||
const Comments = ({ title, className, ...rest }: CommentsProps) => {
|
||||
const Comments = ({ title, ...rest }: CommentsProps) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.wrapper, className)} {...rest}>
|
||||
<Wrapper {...rest}>
|
||||
<Giscus
|
||||
{...(giscusConfig as GiscusProps)}
|
||||
term={title}
|
||||
@ -24,7 +32,7 @@ const Comments = ({ title, className, ...rest }: CommentsProps) => {
|
||||
emitMetadata="0"
|
||||
theme={resolvedTheme === "dark" ? "dark" : "light"}
|
||||
/>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,94 +0,0 @@
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.8em;
|
||||
margin: 0.6em 0;
|
||||
border: 2px solid;
|
||||
border-radius: var(--rounded-edge-radius);
|
||||
color: var(--text);
|
||||
background-color: var(--super-duper-light);
|
||||
border-color: var(--light);
|
||||
|
||||
/* light-dark theme switch fading */
|
||||
transition: background 0.25s ease;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--link);
|
||||
}
|
||||
|
||||
.input.missing {
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
margin-bottom: 0;
|
||||
line-height: 1.5;
|
||||
min-height: 10em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.markdown_tip {
|
||||
font-size: 0.825em;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.action_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 3.75em;
|
||||
}
|
||||
|
||||
.btn_submit {
|
||||
flex-shrink: 0;
|
||||
height: 3.25em;
|
||||
padding: 1em 1.25em;
|
||||
margin-right: 1.5em;
|
||||
border: 0;
|
||||
border-radius: var(--rounded-edge-radius);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
background-color: var(--kinda-light);
|
||||
}
|
||||
|
||||
.btn_submit:hover {
|
||||
color: var(--super-duper-light);
|
||||
background-color: var(--link);
|
||||
}
|
||||
|
||||
.send_icon {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
vertical-align: -0.2em;
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.result_icon {
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
vertical-align: -0.3em;
|
||||
}
|
||||
|
||||
.result_success,
|
||||
.result_error {
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.result_success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.result_error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
@ -1,13 +1,126 @@
|
||||
import { useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import Link from "../Link/Link";
|
||||
import Captcha from "../Captcha/Captcha";
|
||||
import { SendIcon, CheckOcticon, XOcticon } from "../Icons";
|
||||
import { styled, css } from "../../lib/styles/stitches.config";
|
||||
import type { FormikHelpers } from "formik";
|
||||
|
||||
import styles from "./ContactForm.module.css";
|
||||
// CSS applied to both `<input />` and `<textarea />`
|
||||
const InputStyles = css({
|
||||
width: "100%",
|
||||
padding: "0.8em",
|
||||
margin: "0.6em 0",
|
||||
border: "2px solid",
|
||||
borderRadius: "$rounded",
|
||||
color: "$text",
|
||||
backgroundColor: "$superDuperLight",
|
||||
borderColor: "$light",
|
||||
|
||||
// light-dark theme switch fading
|
||||
transition: "background 0.25s ease",
|
||||
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
borderColor: "$link",
|
||||
},
|
||||
|
||||
variants: {
|
||||
missing: {
|
||||
true: {
|
||||
borderColor: "$error",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Input = styled("input", InputStyles);
|
||||
|
||||
const TextArea = styled(TextareaAutosize, InputStyles, {
|
||||
marginBottom: 0,
|
||||
lineHeight: 1.5,
|
||||
minHeight: "10em",
|
||||
resize: "vertical",
|
||||
});
|
||||
|
||||
const MarkdownTip = styled("div", {
|
||||
fontSize: "0.825em",
|
||||
lineHeight: 1.75,
|
||||
});
|
||||
|
||||
const HCaptcha = styled(Captcha, {
|
||||
margin: "1em 0",
|
||||
});
|
||||
|
||||
const ActionRow = styled("div", {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
minHeight: "3.75em",
|
||||
});
|
||||
|
||||
const SubmitButton = styled("button", {
|
||||
flexShrink: 0,
|
||||
height: "3.25em",
|
||||
padding: "1em 1.25em",
|
||||
marginRight: "1.5em",
|
||||
border: "0",
|
||||
borderRadius: "$rounded",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
fontWeight: 500,
|
||||
color: "$text",
|
||||
backgroundColor: "$kindaLight",
|
||||
|
||||
"&:hover": {
|
||||
color: "$superDuperLight",
|
||||
backgroundColor: "$link",
|
||||
},
|
||||
|
||||
variants: {
|
||||
hidden: {
|
||||
true: {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const SubmitIcon = styled(SendIcon, {
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
verticalAlign: "-0.2em",
|
||||
marginRight: "0.4em",
|
||||
});
|
||||
|
||||
const Result = styled("div", {
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.5,
|
||||
|
||||
variants: {
|
||||
status: {
|
||||
success: {
|
||||
color: "$success",
|
||||
},
|
||||
error: {
|
||||
color: "$error",
|
||||
},
|
||||
},
|
||||
|
||||
hidden: {
|
||||
true: {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ResultIcon = styled("svg", {
|
||||
width: "1.3em",
|
||||
height: "1.3em",
|
||||
verticalAlign: "-0.3em",
|
||||
fill: "currentColor",
|
||||
});
|
||||
|
||||
type Values = {
|
||||
name: string;
|
||||
@ -100,11 +213,11 @@ const ContactForm = ({ className }: ContactFormProps) => {
|
||||
<Form className={className} name="contact">
|
||||
<Field name="name">
|
||||
{({ field, meta }) => (
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
className={classNames(styles.input, meta.error && meta.touched && styles.missing)}
|
||||
placeholder="Name"
|
||||
disabled={success}
|
||||
missing={meta.error && meta.touched}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
@ -112,12 +225,12 @@ const ContactForm = ({ className }: ContactFormProps) => {
|
||||
|
||||
<Field name="email">
|
||||
{({ field, meta }) => (
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
inputMode="email"
|
||||
className={classNames(styles.input, meta.error && meta.touched && styles.missing)}
|
||||
placeholder="Email"
|
||||
disabled={success}
|
||||
missing={meta.error && meta.touched}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
@ -125,17 +238,17 @@ const ContactForm = ({ className }: ContactFormProps) => {
|
||||
|
||||
<Field name="message">
|
||||
{({ field, meta }) => (
|
||||
<TextareaAutosize
|
||||
className={classNames(styles.input, styles.textarea, meta.error && meta.touched && styles.missing)}
|
||||
<TextArea
|
||||
placeholder="Write something..."
|
||||
minRows={5}
|
||||
disabled={success}
|
||||
missing={meta.error && meta.touched}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div className={styles.markdown_tip}>
|
||||
<MarkdownTip>
|
||||
Basic{" "}
|
||||
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" style={{ fontWeight: 600 }}>
|
||||
Markdown syntax
|
||||
@ -145,45 +258,32 @@ const ContactForm = ({ className }: ContactFormProps) => {
|
||||
links
|
||||
</Link>
|
||||
](https://jarv.is), and <code>`code`</code>.
|
||||
</div>
|
||||
</MarkdownTip>
|
||||
|
||||
<div className={styles.captcha}>
|
||||
<Captcha onVerify={(token) => setFieldValue("h-captcha-response", token)} />
|
||||
</div>
|
||||
<HCaptcha onVerify={(token) => setFieldValue("h-captcha-response", token)} />
|
||||
|
||||
<div className={styles.action_row}>
|
||||
<button
|
||||
className={classNames(styles.btn_submit, success && styles.hidden)}
|
||||
<ActionRow>
|
||||
<SubmitButton
|
||||
type="submit"
|
||||
title="Send Message"
|
||||
aria-label="Send Message"
|
||||
onClick={() => setSubmitted(true)}
|
||||
disabled={isSubmitting}
|
||||
hidden={success}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span>Sending...</span>
|
||||
) : (
|
||||
<>
|
||||
<SendIcon className={classNames(styles.send_icon)} /> <span>Send</span>
|
||||
<SubmitIcon /> <span>Send</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</SubmitButton>
|
||||
|
||||
<span
|
||||
className={classNames(
|
||||
success && styles.result_success,
|
||||
!success && styles.result_error,
|
||||
(!submitted || !feedback || isSubmitting) && styles.hidden
|
||||
)}
|
||||
>
|
||||
{success ? (
|
||||
<CheckOcticon className={styles.result_icon} fill="CurrentColor" />
|
||||
) : (
|
||||
<XOcticon className={styles.result_icon} fill="CurrentColor" />
|
||||
)}{" "}
|
||||
{feedback}
|
||||
</span>
|
||||
</div>
|
||||
<Result status={success ? "success" : "error"} hidden={!submitted || !feedback || isSubmitting}>
|
||||
<ResultIcon as={success ? CheckOcticon : XOcticon} /> {feedback}
|
||||
</Result>
|
||||
</ActionRow>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
@ -1,12 +0,0 @@
|
||||
.content {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.7;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.content {
|
||||
font-size: 0.925em;
|
||||
line-height: 1.85;
|
||||
}
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
import classNames from "classnames";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
|
||||
import styles from "./Content.module.css";
|
||||
const Content = styled("div", {
|
||||
fontSize: "0.9em",
|
||||
lineHeight: 1.7,
|
||||
color: "$text",
|
||||
|
||||
export type ContentProps = JSX.IntrinsicElements["div"];
|
||||
|
||||
const Content = ({ className, ...rest }: ContentProps) => (
|
||||
<div className={classNames(styles.content, className)} {...rest} />
|
||||
);
|
||||
"@mobile": {
|
||||
fontSize: "0.925em",
|
||||
lineHeight: 1.85,
|
||||
},
|
||||
});
|
||||
|
||||
export default Content;
|
||||
|
@ -1,14 +0,0 @@
|
||||
.button {
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button.success {
|
||||
color: var(--success) !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
vertical-align: -0.3em;
|
||||
}
|
@ -1,11 +1,29 @@
|
||||
import { forwardRef, useState, useEffect } from "react";
|
||||
import classNames from "classnames";
|
||||
import copy from "copy-to-clipboard";
|
||||
import innerText from "react-innertext";
|
||||
import { ClipboardOcticon, CheckOcticon } from "../Icons";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ReactNode, Ref } from "react";
|
||||
|
||||
import styles from "./CopyButton.module.css";
|
||||
const Button = styled("button", {
|
||||
lineHeight: 1,
|
||||
cursor: "pointer",
|
||||
|
||||
variants: {
|
||||
success: {
|
||||
true: {
|
||||
color: "$success !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Icon = styled("svg", {
|
||||
width: "1.25em",
|
||||
height: "1.25em",
|
||||
verticalAlign: "-0.3em",
|
||||
fill: "currentColor",
|
||||
});
|
||||
|
||||
export type CopyButtonProps = {
|
||||
source: ReactNode;
|
||||
@ -45,20 +63,17 @@ const CopyButton = forwardRef(function CopyButton(
|
||||
}, [timeout, copied]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(styles.button, copied && styles.success, className)}
|
||||
<Button
|
||||
className={className}
|
||||
title="Copy to clipboard"
|
||||
aria-label="Copy to clipboard"
|
||||
onClick={handleCopy}
|
||||
disabled={!!copied}
|
||||
success={copied}
|
||||
ref={ref}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckOcticon className={styles.icon} fill="currentColor" />
|
||||
) : (
|
||||
<ClipboardOcticon className={styles.icon} fill="currentColor" />
|
||||
)}
|
||||
</button>
|
||||
<Icon as={copied ? CheckOcticon : ClipboardOcticon} />
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
.figure {
|
||||
margin: 1em auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
color: var(--medium);
|
||||
margin-top: -0.4em;
|
||||
}
|
||||
|
||||
/* some figcaptions contain paragraphs, some don't, so reset all of them */
|
||||
.caption p {
|
||||
margin: 0 !important;
|
||||
}
|
@ -1,10 +1,25 @@
|
||||
import Image from "../Image/Image";
|
||||
import innerText from "react-innertext";
|
||||
import classNames from "classnames";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import type { ImageProps as NextImageProps } from "next/image";
|
||||
|
||||
import styles from "./Figure.module.css";
|
||||
const Wrapper = styled("figure", {
|
||||
margin: "1em auto",
|
||||
textAlign: "center",
|
||||
});
|
||||
|
||||
const Caption = styled("figcaption", {
|
||||
fontSize: "0.9em",
|
||||
lineHeight: 1.5,
|
||||
color: "$medium",
|
||||
marginTop: "-0.4em",
|
||||
|
||||
/* some figcaptions contain paragraphs, some don't, so reset all of them */
|
||||
"& p": {
|
||||
margin: "0 !important",
|
||||
},
|
||||
});
|
||||
|
||||
export type FigureProps = Omit<NextImageProps, "alt"> &
|
||||
PropsWithChildren<{
|
||||
@ -13,10 +28,10 @@ export type FigureProps = Omit<NextImageProps, "alt"> &
|
||||
|
||||
const Figure = ({ children, alt, className, ...imageProps }: FigureProps) => {
|
||||
return (
|
||||
<figure className={classNames(styles.figure, className)}>
|
||||
<Wrapper className={className}>
|
||||
<Image alt={alt || innerText(children)} {...imageProps} />
|
||||
<figcaption className={styles.caption}>{children}</figcaption>
|
||||
</figure>
|
||||
<Caption>{children}</Caption>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,88 +0,0 @@
|
||||
.footer {
|
||||
width: 100%;
|
||||
padding: 1.25em 1.5em;
|
||||
border-top: 1px solid var(--kinda-light);
|
||||
background-color: var(--background-outer);
|
||||
color: var(--medium-dark);
|
||||
|
||||
/* light-dark theme switch fading */
|
||||
transition: color 0.25s ease, background 0.25s ease, border 0.25s ease;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 865px;
|
||||
margin: 0 auto;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85em;
|
||||
line-height: 2.3;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
vertical-align: -0.25em;
|
||||
margin: 0 0.075em;
|
||||
}
|
||||
|
||||
.nextjs:hover {
|
||||
color: var(--medium);
|
||||
}
|
||||
|
||||
.view_source {
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--light);
|
||||
}
|
||||
|
||||
.view_source:hover {
|
||||
border-color: var(--kinda-light);
|
||||
}
|
||||
|
||||
.heart {
|
||||
display: inline-block;
|
||||
animation: beat 10s infinite; /* 6 bpm, call 911 if you see this please. */
|
||||
animation-delay: 7.5s; /* offset from wave animation */
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.footer {
|
||||
padding: 1em 1.25em;
|
||||
}
|
||||
|
||||
/* stack columns on left instead of flexboxing across */
|
||||
.row {
|
||||
font-size: 0.8em;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes beat {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
2% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
4% {
|
||||
transform: scale(1);
|
||||
}
|
||||
6% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
8% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* pause for ~9 out of 10 seconds */
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
@ -1,60 +1,128 @@
|
||||
import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import classNames from "classnames";
|
||||
import NextLink from "next/link";
|
||||
import { HeartIcon, NextjsLogo } from "../Icons";
|
||||
import { keyframes, styled } from "../../lib/styles/stitches.config";
|
||||
import * as config from "../../lib/config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./Footer.module.css";
|
||||
const Wrapper = styled("footer", {
|
||||
width: "100%",
|
||||
padding: "1.25em 1.5em",
|
||||
borderTop: "1px solid $kindaLight",
|
||||
backgroundColor: "$backgroundOuter",
|
||||
color: "$mediumDark",
|
||||
transition: "color 0.25s ease, background 0.25s ease, border 0.25s ease",
|
||||
|
||||
export type FooterProps = JSX.IntrinsicElements["footer"];
|
||||
"@mobile": {
|
||||
padding: "1em 1.25em",
|
||||
},
|
||||
});
|
||||
|
||||
const Footer = ({ className, ...rest }: FooterProps) => (
|
||||
<footer className={classNames(styles.footer, className)} {...rest}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.license}>
|
||||
const Row = styled("div", {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
maxWidth: "865px",
|
||||
margin: "0 auto",
|
||||
justifyContent: "space-between",
|
||||
fontSize: "0.85em",
|
||||
lineHeight: 2.3,
|
||||
|
||||
// stack columns on left instead of flexboxing across
|
||||
"@mobile": {
|
||||
fontSize: "0.8em",
|
||||
display: "block",
|
||||
},
|
||||
});
|
||||
|
||||
const Link = styled("a", {
|
||||
color: "$mediumDark",
|
||||
textDecoration: "none",
|
||||
});
|
||||
|
||||
const NextjsLink = styled(Link, {
|
||||
"&:hover": {
|
||||
color: "$medium",
|
||||
},
|
||||
});
|
||||
|
||||
const ViewSourceLink = styled(Link, {
|
||||
paddingBottom: "2px",
|
||||
borderBottom: "1px solid",
|
||||
borderColor: "$light",
|
||||
|
||||
"&:hover": {
|
||||
borderColor: "$kindaLight",
|
||||
},
|
||||
});
|
||||
|
||||
const beat = keyframes({
|
||||
"0%": { transform: "scale(1)" },
|
||||
"2%": { transform: "scale(1.25)" },
|
||||
"4%": { transform: "scale(1)" },
|
||||
"6%": { transform: "scale(1.2)" },
|
||||
"8%": { transform: "scale(1)" },
|
||||
// pause for ~9 out of 10 seconds
|
||||
"100%": { transform: "scale(1)" },
|
||||
});
|
||||
|
||||
const Heart = styled("span", {
|
||||
display: "inline-block",
|
||||
animation: `${beat} 10s infinite`,
|
||||
animationDelay: "7.5s",
|
||||
willChange: "transform",
|
||||
});
|
||||
|
||||
const Icon = styled("svg", {
|
||||
width: "1.25em",
|
||||
height: "1.25em",
|
||||
verticalAlign: "-0.25em",
|
||||
margin: "0 0.075em",
|
||||
});
|
||||
|
||||
export type FooterProps = ComponentProps<typeof Wrapper>;
|
||||
|
||||
const Footer = ({ ...rest }: FooterProps) => (
|
||||
<Wrapper {...rest}>
|
||||
<Row>
|
||||
<div>
|
||||
Content{" "}
|
||||
<Link href="/license/" prefetch={false}>
|
||||
<a className={styles.link} title="Creative Commons Attribution 4.0 International">
|
||||
licensed under CC-BY-4.0
|
||||
</a>
|
||||
</Link>
|
||||
<NextLink href="/license/" prefetch={false} passHref={true}>
|
||||
<Link title="Creative Commons Attribution 4.0 International">licensed under CC-BY-4.0</Link>
|
||||
</NextLink>
|
||||
,{" "}
|
||||
<Link href="/previously/" prefetch={false}>
|
||||
<a className={styles.link} title="Previously on...">
|
||||
2001
|
||||
</a>
|
||||
</Link>{" "}
|
||||
<NextLink href="/previously/" prefetch={false} passHref={true}>
|
||||
<Link title="Previously on...">2001</Link>
|
||||
</NextLink>{" "}
|
||||
– {new Date().getFullYear()}.
|
||||
</div>
|
||||
<div className={styles.powered_by}>
|
||||
|
||||
<div>
|
||||
Made with{" "}
|
||||
<span className={styles.heart} title="Love">
|
||||
<HeartIcon className={styles.icon} />
|
||||
</span>{" "}
|
||||
<Heart title="Love">
|
||||
<Icon as={HeartIcon} />
|
||||
</Heart>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
className={classNames(styles.link, styles.nextjs)}
|
||||
<NextjsLink
|
||||
href="https://nextjs.org/"
|
||||
title="Powered by Next.js"
|
||||
aria-label="Next.js"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<NextjsLogo className={styles.icon} fill="currentColor" />
|
||||
</a>
|
||||
<Icon as={NextjsLogo} fill="currentColor" />
|
||||
</NextjsLink>
|
||||
.{" "}
|
||||
<a
|
||||
className={classNames(styles.link, styles.view_source)}
|
||||
<ViewSourceLink
|
||||
href={`https://github.com/${config.githubRepo}`}
|
||||
title="View Source on GitHub"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View source.
|
||||
</a>
|
||||
</ViewSourceLink>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</Row>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default memo(Footer);
|
||||
|
@ -1,43 +0,0 @@
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 4.5em;
|
||||
padding: 0.7em 1.5em;
|
||||
border-bottom: 1px solid var(--kinda-light);
|
||||
background-color: var(--background-header);
|
||||
|
||||
/* light-dark theme switch fading */
|
||||
transition: color 0.25s ease, background 0.25s ease, border 0.25s ease;
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
backdrop-filter: saturate(180%) blur(5px);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 865px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0.75em 1.25em;
|
||||
height: 5.9em;
|
||||
}
|
||||
|
||||
.menu {
|
||||
max-width: 325px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
.menu {
|
||||
max-width: 225px;
|
||||
}
|
||||
}
|
@ -1,21 +1,68 @@
|
||||
import { memo } from "react";
|
||||
import classNames from "classnames";
|
||||
import Selfie from "../Selfie/Selfie";
|
||||
import Menu from "../Menu/Menu";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./Header.module.css";
|
||||
const Wrapper = styled("header", {
|
||||
width: "100%",
|
||||
height: "4.5em",
|
||||
padding: "0.7em 1.5em",
|
||||
borderBottom: "1px solid $kindaLight",
|
||||
backgroundColor: "$backgroundHeader",
|
||||
|
||||
export type HeaderProps = JSX.IntrinsicElements["header"] & {
|
||||
// light-dark theme switch fading
|
||||
transition: "color 0.25s ease, background 0.25s ease, border 0.25s ease",
|
||||
|
||||
"@mobile": {
|
||||
padding: "0.75em 1.25em",
|
||||
height: "5.9em",
|
||||
},
|
||||
|
||||
variants: {
|
||||
sticky: {
|
||||
true: {
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
|
||||
// blurry glass-like background effect (except on firefox)
|
||||
backdropFilter: "saturate(180%) blur(5px)",
|
||||
zIndex: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Nav = styled("nav", {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
maxWidth: "865px",
|
||||
margin: "0 auto",
|
||||
});
|
||||
|
||||
const ResponsiveMenu = styled(Menu, {
|
||||
"@mobile": {
|
||||
maxWidth: "325px",
|
||||
},
|
||||
|
||||
"@superNarrow": {
|
||||
maxWidth: "225px",
|
||||
},
|
||||
});
|
||||
|
||||
export type HeaderProps = ComponentProps<typeof Wrapper> & {
|
||||
sticky?: boolean;
|
||||
};
|
||||
|
||||
const Header = ({ sticky, className, ...rest }: HeaderProps) => (
|
||||
<header className={classNames(styles.header, sticky && styles.sticky, className)} {...rest}>
|
||||
<nav className={styles.nav}>
|
||||
<Selfie className={styles.selfie} />
|
||||
<Menu className={styles.menu} />
|
||||
</nav>
|
||||
</header>
|
||||
const Header = ({ sticky, ...rest }: HeaderProps) => (
|
||||
<Wrapper sticky={sticky} {...rest}>
|
||||
<Nav>
|
||||
<Selfie />
|
||||
<ResponsiveMenu />
|
||||
</Nav>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default memo(Header);
|
||||
|
@ -1,48 +0,0 @@
|
||||
.heading {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.5;
|
||||
|
||||
/**
|
||||
* offset (approximately) with sticky header so jumped-to content isn't hiding behind it.
|
||||
* note: use rem so it isn't based on the heading's font size.
|
||||
*/
|
||||
scroll-margin-top: 5.5rem;
|
||||
}
|
||||
|
||||
/* special bottom border for <h2>s */
|
||||
.h2 {
|
||||
padding-bottom: 0.25em;
|
||||
border-bottom: 1px solid var(--kinda-light);
|
||||
}
|
||||
|
||||
/* sub-heading anchor styles */
|
||||
.anchor {
|
||||
margin: 0 0.25em;
|
||||
padding: 0 0.25em;
|
||||
color: var(--medium-light);
|
||||
font-weight: 300;
|
||||
text-decoration: none;
|
||||
opacity: 0; /* overridden on hover below (except on small screens) */
|
||||
}
|
||||
.anchor::before {
|
||||
content: "\0023"; /* pound sign `#`, done here to keep content DOM cleaner */
|
||||
}
|
||||
.anchor:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
/* make anchor link show up on hover over its corresponding heading */
|
||||
.heading:hover .anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.heading {
|
||||
scroll-margin-top: 6.5rem;
|
||||
}
|
||||
|
||||
/* don't require hover to show anchor link on small (likely touch) screens */
|
||||
.anchor {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -1,29 +1,71 @@
|
||||
import classNames from "classnames";
|
||||
import innerText from "react-innertext";
|
||||
import type { HTMLAttributes } from "react";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./Heading.module.css";
|
||||
const Anchor = styled("a", {
|
||||
margin: "0 0.25em",
|
||||
padding: "0 0.25em",
|
||||
color: "$mediumLight",
|
||||
fontWeight: 300,
|
||||
textDecoration: "none",
|
||||
opacity: 0, // overridden on hover below (except on small screens)
|
||||
|
||||
export type HeadingProps = HTMLAttributes<HTMLHeadingElement> & {
|
||||
"&::before": {
|
||||
// pound sign `#`, done here to keep content DOM cleaner
|
||||
content: "\\0023",
|
||||
},
|
||||
|
||||
"&:hover": {
|
||||
color: "$link",
|
||||
},
|
||||
|
||||
// don't require hover to show anchor link on small (likely touch) screens
|
||||
"@mobile": {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const H = styled("h1", {
|
||||
marginTop: "1em",
|
||||
marginBottom: "0.5em",
|
||||
lineHeight: 1.5,
|
||||
|
||||
// offset (approximately) with sticky header so jumped-to content isn't hiding behind it.
|
||||
// note: use rem so it isn't based on the heading's font size.
|
||||
scrollMarginTop: "5.5rem",
|
||||
|
||||
"@mobile": {
|
||||
scrollMarginTop: "6.5rem",
|
||||
},
|
||||
|
||||
[`&:hover ${Anchor}`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
variants: {
|
||||
underline: {
|
||||
true: {
|
||||
paddingBottom: "0.25em",
|
||||
borderBottom: "1px solid $kindaLight",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type HeadingProps = ComponentProps<typeof H> & {
|
||||
as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
};
|
||||
|
||||
const Heading = ({ as: Component, id, className, children, ...rest }: HeadingProps) => {
|
||||
const Heading = ({ as, id, children, ...rest }: HeadingProps) => {
|
||||
return (
|
||||
<Component className={classNames(styles.heading, styles[Component], className)} id={id} {...rest}>
|
||||
<H as={as} underline={as === "h2"} id={id} {...rest}>
|
||||
{children}
|
||||
|
||||
{/* add anchor link to H2s and H3s. ID is already generated by rehype-slug. `#` character inserted via CSS. */}
|
||||
{id && (Component === "h2" || Component === "h3") && (
|
||||
<a
|
||||
className={styles.anchor}
|
||||
href={`#${id}`}
|
||||
title={`Jump to "${innerText(children)}"`}
|
||||
tabIndex={-1}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
{id && (as === "h2" || as === "h3") && (
|
||||
<Anchor href={`#${id}`} title={`Jump to "${innerText(children)}"`} tabIndex={-1} aria-hidden={true} />
|
||||
)}
|
||||
</Component>
|
||||
</H>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
.hr {
|
||||
margin: 1.5em auto;
|
||||
height: 0.175em;
|
||||
border: 0;
|
||||
background-color: var(--light);
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
import classNames from "classnames";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
|
||||
import styles from "./HorizontalRule.module.css";
|
||||
|
||||
export type HorizontalRuleProps = JSX.IntrinsicElements["hr"];
|
||||
|
||||
const HorizontalRule = ({ className, ...rest }: HorizontalRuleProps) => (
|
||||
<hr className={classNames(styles.hr, className)} {...rest} />
|
||||
);
|
||||
const HorizontalRule = styled("hr", {
|
||||
margin: "1.5em auto",
|
||||
height: "0.175em",
|
||||
border: 0,
|
||||
backgroundColor: "$light",
|
||||
});
|
||||
|
||||
export default HorizontalRule;
|
||||
|
@ -1,7 +0,0 @@
|
||||
.frame {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
border: 2px solid var(--kinda-light);
|
||||
border-radius: var(--rounded-edge-radius);
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
import classNames from "classnames";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./IFrame.module.css";
|
||||
const RoundedIFrame = styled("iframe", {
|
||||
width: "100%",
|
||||
display: "block",
|
||||
margin: "1em auto",
|
||||
border: "2px solid $kindaLight",
|
||||
borderRadius: "$rounded",
|
||||
});
|
||||
|
||||
export type IFrameProps = JSX.IntrinsicElements["iframe"] & {
|
||||
export type IFrameProps = ComponentProps<typeof RoundedIFrame> & {
|
||||
src: string;
|
||||
height: number;
|
||||
width?: number; // defaults to 100%
|
||||
@ -10,17 +17,16 @@ export type IFrameProps = JSX.IntrinsicElements["iframe"] & {
|
||||
noScroll?: boolean;
|
||||
};
|
||||
|
||||
const IFrame = ({ src, title, height, width, allowScripts, noScroll, className, ...rest }: IFrameProps) => (
|
||||
<iframe
|
||||
className={classNames(styles.frame, className)}
|
||||
const IFrame = ({ src, title, height, width, allowScripts, noScroll, ...rest }: IFrameProps) => (
|
||||
<RoundedIFrame
|
||||
src={src}
|
||||
title={title}
|
||||
sandbox={allowScripts ? "allow-same-origin allow-scripts allow-popups" : undefined}
|
||||
scrolling={noScroll ? "no" : undefined}
|
||||
loading="lazy"
|
||||
style={{
|
||||
css={{
|
||||
height: `${height}px`,
|
||||
maxWidth: width ? `${width}px` : undefined,
|
||||
maxWidth: width ? `${width}px` : null,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
|
@ -1,11 +0,0 @@
|
||||
.wrapper {
|
||||
line-height: 0;
|
||||
|
||||
/* default to centering all images */
|
||||
margin: 1em auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: var(--rounded-edge-radius);
|
||||
}
|
@ -1,8 +1,19 @@
|
||||
import NextImage from "next/image";
|
||||
import classNames from "classnames";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { ImageProps as NextImageProps, StaticImageData } from "next/image";
|
||||
|
||||
import styles from "./Image.module.css";
|
||||
const Wrapper = styled("div", {
|
||||
lineHeight: 0,
|
||||
|
||||
// default to centering all images
|
||||
margin: "1em auto",
|
||||
textAlign: "center",
|
||||
});
|
||||
|
||||
const RoundedImage = styled(NextImage, {
|
||||
borderRadius: "$rounded",
|
||||
});
|
||||
|
||||
const CustomImage = ({
|
||||
src,
|
||||
@ -15,7 +26,7 @@ const CustomImage = ({
|
||||
priority,
|
||||
className,
|
||||
...rest
|
||||
}: NextImageProps) => {
|
||||
}: NextImageProps & ComponentProps<typeof RoundedImage>) => {
|
||||
// passed directly into next/image: https://nextjs.org/docs/api-reference/next/image
|
||||
const imageProps: Partial<NextImageProps> = {
|
||||
width: typeof width === "string" ? Number.parseInt(width) : width,
|
||||
@ -40,10 +51,10 @@ const CustomImage = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.wrapper, className)}>
|
||||
<Wrapper className={className}>
|
||||
{/* @ts-ignore */}
|
||||
<NextImage className={styles.image} {...imageProps} {...rest} />
|
||||
</div>
|
||||
<RoundedImage {...imageProps} {...rest} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,27 +0,0 @@
|
||||
.flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.default {
|
||||
width: 100%;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 865px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* footer needs to fill the remaining vertical screen space. doing it here to keep flex stuff together. */
|
||||
.footer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.main {
|
||||
padding: 1.25em;
|
||||
}
|
||||
}
|
@ -1,41 +1,66 @@
|
||||
import Head from "next/head";
|
||||
import { useTheme } from "next-themes";
|
||||
import classNames from "classnames";
|
||||
import Header from "../Header/Header";
|
||||
import Footer from "../Footer/Footer";
|
||||
import themes from "../../lib/config/themes";
|
||||
import { styled, theme, darkTheme } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./Layout.module.css";
|
||||
const Flex = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "100vh",
|
||||
});
|
||||
|
||||
export type LayoutProps = JSX.IntrinsicElements["div"] & {
|
||||
const Default = styled("main", {
|
||||
width: "100%",
|
||||
padding: "1.5em",
|
||||
});
|
||||
|
||||
const Container = styled("div", {
|
||||
maxWidth: "865px",
|
||||
margin: "0 auto",
|
||||
display: "block",
|
||||
});
|
||||
|
||||
// footer needs to fill the remaining vertical screen space. doing it here to keep flex stuff together.
|
||||
const FlexedFooter = styled(Footer, {
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export type LayoutProps = ComponentProps<typeof Flex> & {
|
||||
container?: boolean; // pass false to disable default `<main>` container styles with padding, etc.
|
||||
stickyHeader?: boolean; // pass false to override default stickiness of header when scrolling
|
||||
};
|
||||
|
||||
const Layout = ({ container = true, stickyHeader = true, className, children, ...rest }: LayoutProps) => {
|
||||
const Layout = ({ container = true, stickyHeader = true, children, ...rest }: LayoutProps) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{/* dynamically set browser theme color to match the background color; default to light for SSR */}
|
||||
<meta name="theme-color" content={themes[resolvedTheme || "light"].backgroundOuter} />
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={
|
||||
resolvedTheme === "dark" ? darkTheme.colors.backgroundOuter.value : theme.colors.backgroundOuter.value
|
||||
}
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<div className={classNames(styles.flex, className)} {...rest}>
|
||||
<Flex {...rest}>
|
||||
<Header sticky={stickyHeader} />
|
||||
|
||||
{/* passing `container={false}` to Layout allows 100% control of the content area on a per-page basis */}
|
||||
{container ? (
|
||||
<main className={styles.default}>
|
||||
<div className={styles.container}>{children}</div>
|
||||
</main>
|
||||
<Default>
|
||||
<Container>{children}</Container>
|
||||
</Default>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
|
||||
<Footer className={styles.footer} />
|
||||
</div>
|
||||
<FlexedFooter />
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,16 +0,0 @@
|
||||
.link {
|
||||
color: var(--link);
|
||||
background-image: linear-gradient(var(--link-underline), var(--link-underline));
|
||||
background-position: 0% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0% var(--link-underline-size);
|
||||
text-decoration: none;
|
||||
padding-bottom: 0.2rem;
|
||||
|
||||
/* background-size is for hover animation, color & border are for theme fading: */
|
||||
transition: background-size 0.25s ease-in-out, color 0.25s ease, border 0.25s ease;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
background-size: 100% var(--link-underline-size);
|
||||
}
|
@ -1,11 +1,28 @@
|
||||
import NextLink from "next/link";
|
||||
import classNames from "classnames";
|
||||
import isAbsoluteUrl from "is-absolute-url";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { LinkProps as NextLinkProps } from "next/link";
|
||||
|
||||
import styles from "./Link.module.css";
|
||||
const FancyLink = styled("a", {
|
||||
color: "$link",
|
||||
textDecoration: "none",
|
||||
transition: "background-size 0.25s ease-in-out, color 0.25s ease, border 0.25s ease",
|
||||
|
||||
export type CustomLinkProps = Omit<JSX.IntrinsicElements["a"], "href"> &
|
||||
backgroundPosition: "0% 100%",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "0% $underline",
|
||||
paddingBottom: "0.2rem",
|
||||
|
||||
// sets psuedo linear-gradient() for cool underline effect
|
||||
backgroundGradientHack: {},
|
||||
|
||||
"&:hover": {
|
||||
backgroundSize: "100% $underline",
|
||||
},
|
||||
});
|
||||
|
||||
export type CustomLinkProps = Omit<ComponentProps<typeof FancyLink>, "href"> &
|
||||
NextLinkProps & {
|
||||
forceNewWindow?: boolean;
|
||||
};
|
||||
@ -17,25 +34,18 @@ const CustomLink = ({
|
||||
target,
|
||||
rel,
|
||||
forceNewWindow,
|
||||
className,
|
||||
...rest
|
||||
}: CustomLinkProps) => {
|
||||
// this component auto-detects whether or not we should use a normal HTML anchor externally or next/link internally,
|
||||
// can be overridden with `forceNewWindow={true}`.
|
||||
if (forceNewWindow || isAbsoluteUrl(href.toString())) {
|
||||
return (
|
||||
<a
|
||||
href={href.toString()}
|
||||
target={target || "_blank"}
|
||||
rel={rel || "noopener noreferrer"}
|
||||
className={classNames(styles.link, className)}
|
||||
{...rest}
|
||||
/>
|
||||
<FancyLink href={href.toString()} target={target || "_blank"} rel={rel || "noopener noreferrer"} {...rest} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NextLink href={href} prefetch={prefetch} passHref={passHref}>
|
||||
<a className={classNames(styles.link, className)} {...rest} />
|
||||
<FancyLink {...rest} />
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
.unordered,
|
||||
.ordered {
|
||||
margin-left: 1.5em;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding-left: 0.25em;
|
||||
}
|
@ -1,15 +1,13 @@
|
||||
import classNames from "classnames";
|
||||
import { styled, css } from "../../lib/styles/stitches.config";
|
||||
|
||||
import styles from "./List.module.css";
|
||||
const ListStyles = css({
|
||||
marginLeft: "1.5em",
|
||||
paddingLeft: 0,
|
||||
});
|
||||
|
||||
export const UnorderedList = ({ className, ...rest }: JSX.IntrinsicElements["ul"]) => (
|
||||
<ul className={classNames(styles.unordered, className)} {...rest} />
|
||||
);
|
||||
export const OrderedList = ({ className, ...rest }: JSX.IntrinsicElements["ol"]) => (
|
||||
<ol className={classNames(styles.ordered, className)} {...rest} />
|
||||
);
|
||||
export const UnorderedList = styled("ul", ListStyles);
|
||||
export const OrderedList = styled("ol", ListStyles);
|
||||
|
||||
// TODO: this is based on good faith that the children are all `<li>`s...
|
||||
export const ListItem = ({ className, ...rest }: JSX.IntrinsicElements["li"]) => (
|
||||
<li className={classNames(styles.item, className)} {...rest} />
|
||||
);
|
||||
export const ListItem = styled("li", {
|
||||
paddingLeft: "0.25em",
|
||||
});
|
||||
|
@ -1,22 +0,0 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
animation: loading 1.5s infinite ease-in-out both;
|
||||
background-color: var(--medium-light);
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
40% {
|
||||
transform: scale(0.6);
|
||||
}
|
||||
}
|
@ -1,7 +1,26 @@
|
||||
import { memo } from "react";
|
||||
import classNames from "classnames";
|
||||
import { styled, keyframes } from "../../lib/styles/stitches.config";
|
||||
|
||||
import styles from "./Loading.module.css";
|
||||
const pulse = keyframes({
|
||||
"0%, 80%, 100%": {
|
||||
transform: "scale(0)",
|
||||
},
|
||||
"40%": {
|
||||
transform: "scale(0.6)",
|
||||
},
|
||||
});
|
||||
|
||||
const Wrapper = styled("div", {
|
||||
display: "inline-block",
|
||||
textAlign: "center",
|
||||
});
|
||||
|
||||
const Box = styled("div", {
|
||||
display: "inline-block",
|
||||
height: "100%",
|
||||
animation: `${pulse} 1.5s infinite ease-in-out both`,
|
||||
backgroundColor: "$mediumLight",
|
||||
});
|
||||
|
||||
export type LoadingProps = {
|
||||
width: number; // of entire container, in pixels
|
||||
@ -19,10 +38,9 @@ const Loading = ({ width, boxes = 3, timing = 0.1, className }: LoadingProps) =>
|
||||
// width of each box correlates with number of boxes (with a little padding)
|
||||
// each individual box's animation has a staggered start in corresponding order
|
||||
divs.push(
|
||||
<div
|
||||
<Box
|
||||
key={i}
|
||||
className={styles.box}
|
||||
style={{
|
||||
css={{
|
||||
width: `${width / (boxes + 1)}px`,
|
||||
animationDelay: `${i * timing}s`,
|
||||
}}
|
||||
@ -31,15 +49,15 @@ const Loading = ({ width, boxes = 3, timing = 0.1, className }: LoadingProps) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.wrapper, className)}
|
||||
<Wrapper
|
||||
className={className}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${width / 2}px`,
|
||||
}}
|
||||
>
|
||||
{divs}
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,34 +0,0 @@
|
||||
.menu {
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
list-style: none;
|
||||
display: inline-flex;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.menu {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
.menu {
|
||||
margin-left: 1.4em;
|
||||
}
|
||||
|
||||
/* the home icon is redundant when space is SUPER tight */
|
||||
.item:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,27 +1,57 @@
|
||||
import { memo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import classNames from "classnames";
|
||||
import MenuItem from "../MenuItem/MenuItem";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import { menuItems } from "../../lib/config/menu";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./Menu.module.css";
|
||||
const Wrapper = styled("ul", {
|
||||
display: "inline-flex",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
|
||||
export type MenuProps = {
|
||||
className?: string;
|
||||
};
|
||||
"@mobile": {
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
marginLeft: "1em",
|
||||
},
|
||||
|
||||
const Menu = ({ className }: MenuProps) => {
|
||||
"@superNarrow": {
|
||||
marginLeft: "1.4em",
|
||||
},
|
||||
});
|
||||
|
||||
const Item = styled("li", {
|
||||
listStyle: "none",
|
||||
display: "inline-flex",
|
||||
marginLeft: "1em",
|
||||
|
||||
"@mobile": {
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
"@superNarrow": {
|
||||
// the home icon is kinda redundant when space is SUPER tight
|
||||
"&:first-of-type": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type MenuProps = ComponentProps<typeof Wrapper>;
|
||||
|
||||
const Menu = ({ ...rest }: MenuProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<ul className={classNames(styles.menu, className)}>
|
||||
<Wrapper {...rest}>
|
||||
{menuItems.map((item, index) => (
|
||||
<li key={index} className={styles.item}>
|
||||
<Item key={index}>
|
||||
{/* kinda weird/hacky way to determine if the *first part* of the current path matches this href */}
|
||||
<MenuItem {...item} current={item.href === `/${router.pathname.split("/")[1]}`} />
|
||||
</li>
|
||||
</Item>
|
||||
))}
|
||||
</ul>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,47 +0,0 @@
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--medium-dark);
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
padding: 0.6em;
|
||||
}
|
||||
|
||||
.link:hover,
|
||||
.link.current {
|
||||
border-bottom: 0.2em solid;
|
||||
margin-bottom: -0.2em;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
border-color: var(--kinda-light);
|
||||
}
|
||||
|
||||
.link.current {
|
||||
border-color: var(--link-underline);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
vertical-align: -0.3em;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.95em;
|
||||
font-weight: 500;
|
||||
margin-top: 0.1em;
|
||||
margin-left: 0.8em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
}
|
||||
|
||||
/* hide text next to emojis on mobile */
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,7 +1,52 @@
|
||||
import Link from "next/link";
|
||||
import classNames from "classnames";
|
||||
import NextLink from "next/link";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
|
||||
import styles from "./MenuItem.module.css";
|
||||
const Link = styled("a", {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
color: "$mediumDark",
|
||||
lineHeight: 1,
|
||||
textDecoration: "none",
|
||||
padding: "0.6em",
|
||||
|
||||
"&:hover": {
|
||||
borderBottom: "0.2em solid",
|
||||
marginBottom: "-0.2em",
|
||||
borderColor: "$kindaLight",
|
||||
},
|
||||
|
||||
variants: {
|
||||
current: {
|
||||
true: {
|
||||
borderBottom: "0.2em solid",
|
||||
marginBottom: "-0.2em",
|
||||
borderColor: "$linkUnderline !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Label = styled("span", {
|
||||
fontSize: "0.95em",
|
||||
fontWeight: 500,
|
||||
marginTop: "0.1em",
|
||||
marginLeft: "0.8em",
|
||||
|
||||
"@mobile": {
|
||||
display: "none",
|
||||
},
|
||||
});
|
||||
|
||||
const Icon = styled("svg", {
|
||||
width: "1.25em",
|
||||
height: "1.25em",
|
||||
verticalAlign: "-0.3em",
|
||||
|
||||
"@mobile": {
|
||||
width: "1.8em",
|
||||
height: "1.8em",
|
||||
},
|
||||
});
|
||||
|
||||
export type MenuItemProps = {
|
||||
href?: string;
|
||||
@ -14,19 +59,21 @@ export type MenuItemProps = {
|
||||
icon: any;
|
||||
};
|
||||
|
||||
const MenuItem = ({ icon: Icon, href, text, current, className }: MenuItemProps) => {
|
||||
const MenuItem = ({ icon: ItemIcon, href, text, current, className }: MenuItemProps) => {
|
||||
const linkContent = (
|
||||
<>
|
||||
<Icon className={classNames(styles.icon, className)} /> {text && <span className={styles.label}>{text}</span>}
|
||||
<Icon as={ItemIcon} /> {text && <Label>{text}</Label>}
|
||||
</>
|
||||
);
|
||||
|
||||
// allow both navigational links and/or other interactive react components (e.g. the theme toggle)
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} prefetch={false}>
|
||||
<a className={classNames(styles.link, current && styles.current, className)}>{linkContent}</a>
|
||||
</Link>
|
||||
<NextLink href={href} prefetch={false} passHref={true}>
|
||||
<Link className={className} current={current}>
|
||||
{linkContent}
|
||||
</Link>
|
||||
</NextLink>
|
||||
);
|
||||
} else {
|
||||
return linkContent;
|
||||
|
@ -1,55 +0,0 @@
|
||||
.meta {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.825em;
|
||||
line-height: 2.3;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--medium);
|
||||
}
|
||||
|
||||
.meta_item {
|
||||
display: inline-flex;
|
||||
margin-right: 1.6em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
vertical-align: -0.2em;
|
||||
margin-right: 0.6em;
|
||||
}
|
||||
|
||||
.tags {
|
||||
white-space: normal;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
text-transform: lowercase;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
|
||||
.tag::before {
|
||||
content: "\0023"; /* cosmetically hashtagify tags */
|
||||
padding-right: 0.125em;
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.tag:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.date_link,
|
||||
.edit_link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.views {
|
||||
/* fix potential layout shift when number of hits loads */
|
||||
min-width: 7em;
|
||||
margin-right: 0;
|
||||
}
|
@ -1,73 +1,121 @@
|
||||
import Link from "next/link";
|
||||
import classNames from "classnames";
|
||||
import { format } from "date-fns";
|
||||
import HitCounter from "../HitCounter/HitCounter";
|
||||
import NoteTitle from "../NoteTitle/NoteTitle";
|
||||
import { DateIcon, TagIcon, EditIcon, ViewsIcon } from "../Icons";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import * as config from "../../lib/config";
|
||||
import type { NoteType } from "../../types";
|
||||
|
||||
import styles from "./NoteMeta.module.css";
|
||||
const Wrapper = styled("div", {
|
||||
display: "inline-flex",
|
||||
flexWrap: "wrap",
|
||||
fontSize: "0.825em",
|
||||
lineHeight: 2.3,
|
||||
letterSpacing: "0.04em",
|
||||
color: "$medium",
|
||||
});
|
||||
|
||||
const MetaItem = styled("div", {
|
||||
display: "inline-flex",
|
||||
marginRight: "1.6em",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
const MetaLink = styled("a", {
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
});
|
||||
|
||||
const Icon = styled("svg", {
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
verticalAlign: "-0.2em",
|
||||
marginRight: "0.6em",
|
||||
});
|
||||
|
||||
const Tag = styled("span", {
|
||||
textTransform: "lowercase",
|
||||
whiteSpace: "nowrap",
|
||||
marginRight: "0.75em",
|
||||
|
||||
"&::before": {
|
||||
content: "\\0023", // cosmetically hashtagify tags
|
||||
paddingRight: "0.125em",
|
||||
color: "$light",
|
||||
},
|
||||
|
||||
"&:last-of-type": {
|
||||
marginRight: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export type NoteMetaProps = Pick<NoteType["frontMatter"], "slug" | "date" | "title" | "htmlTitle" | "tags">;
|
||||
|
||||
const NoteMeta = ({ slug, date, title, htmlTitle, tags = [] }: NoteMetaProps) => (
|
||||
<>
|
||||
<div className={styles.meta}>
|
||||
<div className={styles.meta_item}>
|
||||
<Wrapper>
|
||||
<MetaItem>
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/notes/[slug]/",
|
||||
query: { slug },
|
||||
}}
|
||||
passHref={true}
|
||||
>
|
||||
<a className={styles.date_link}>
|
||||
<MetaLink>
|
||||
<span>
|
||||
<DateIcon className={styles.icon} />
|
||||
<Icon as={DateIcon} />
|
||||
</span>
|
||||
<span title={format(new Date(date), "PPppp")}>{format(new Date(date), "MMMM d, yyyy")}</span>
|
||||
</a>
|
||||
</MetaLink>
|
||||
</Link>
|
||||
</div>
|
||||
</MetaItem>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className={classNames(styles.meta_item, styles.tags)}>
|
||||
<MetaItem
|
||||
css={{
|
||||
whiteSpace: "normal",
|
||||
display: "inline-flex",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<TagIcon className={styles.icon} />
|
||||
<Icon as={TagIcon} />
|
||||
</span>
|
||||
{tags.map((tag) => (
|
||||
<span key={tag} className={styles.tag}>
|
||||
{tag}
|
||||
</span>
|
||||
<Tag key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</MetaItem>
|
||||
)}
|
||||
|
||||
<div className={styles.meta_item}>
|
||||
<a
|
||||
className={styles.edit_link}
|
||||
<MetaItem>
|
||||
<MetaLink
|
||||
href={`https://github.com/${config.githubRepo}/blob/main/notes/${slug}.mdx`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`Edit "${title}" on GitHub`}
|
||||
>
|
||||
<span>
|
||||
<EditIcon className={styles.icon} />
|
||||
<Icon as={EditIcon} />
|
||||
</span>
|
||||
<span>Improve This Post</span>
|
||||
</a>
|
||||
</div>
|
||||
</MetaLink>
|
||||
</MetaItem>
|
||||
|
||||
{/* only count hits on production site */}
|
||||
{process.env.NEXT_PUBLIC_VERCEL_ENV === "production" && (
|
||||
<div className={classNames(styles.meta_item, styles.views)}>
|
||||
<MetaItem
|
||||
// fix potential layout shift when number of hits loads
|
||||
css={{ minWidth: "7em", marginRight: 0 }}
|
||||
>
|
||||
<span>
|
||||
<ViewsIcon className={styles.icon} />
|
||||
<Icon as={ViewsIcon} />
|
||||
</span>
|
||||
<HitCounter slug={`notes/${slug}`} />
|
||||
</div>
|
||||
</MetaItem>
|
||||
)}
|
||||
</div>
|
||||
</Wrapper>
|
||||
|
||||
<NoteTitle slug={slug} htmlTitle={htmlTitle || title} />
|
||||
</>
|
||||
|
@ -1,21 +0,0 @@
|
||||
.title {
|
||||
margin: 0.3em 0 0.5em -0.03em;
|
||||
font-size: 2.1em;
|
||||
line-height: 1.3;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.title code {
|
||||
margin: 0 0.075em;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
@ -1,22 +1,42 @@
|
||||
import Link from "next/link";
|
||||
import classNames from "classnames";
|
||||
import NextLink from "next/link";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { NoteType } from "../../types";
|
||||
|
||||
import styles from "./NoteTitle.module.css";
|
||||
const Title = styled("h1", {
|
||||
margin: "0.3em 0 0.5em -0.03em",
|
||||
fontSize: "2.1em",
|
||||
lineHeight: 1.3,
|
||||
fontWeight: 700,
|
||||
|
||||
export type NoteTitleProps = Pick<NoteType["frontMatter"], "slug" | "htmlTitle"> & JSX.IntrinsicElements["h1"];
|
||||
"& code": {
|
||||
margin: "0 0.075em",
|
||||
},
|
||||
|
||||
const NoteTitle = ({ slug, htmlTitle, className, ...rest }: NoteTitleProps) => (
|
||||
<h1 className={classNames(styles.title, className)} {...rest}>
|
||||
<Link
|
||||
"@mobile": {
|
||||
fontSize: "1.8em",
|
||||
},
|
||||
});
|
||||
|
||||
const Link = styled("a", {
|
||||
color: "$text",
|
||||
textDecoration: "none",
|
||||
});
|
||||
|
||||
export type NoteTitleProps = Pick<NoteType["frontMatter"], "slug" | "htmlTitle"> & ComponentProps<typeof Title>;
|
||||
|
||||
const NoteTitle = ({ slug, htmlTitle, ...rest }: NoteTitleProps) => (
|
||||
<Title {...rest}>
|
||||
<NextLink
|
||||
href={{
|
||||
pathname: "/notes/[slug]/",
|
||||
query: { slug },
|
||||
}}
|
||||
passHref={true}
|
||||
>
|
||||
<a className={styles.link} dangerouslySetInnerHTML={{ __html: htmlTitle }} />
|
||||
</Link>
|
||||
</h1>
|
||||
<Link dangerouslySetInnerHTML={{ __html: htmlTitle }} />
|
||||
</NextLink>
|
||||
</Title>
|
||||
);
|
||||
|
||||
export default NoteTitle;
|
||||
|
@ -1,51 +0,0 @@
|
||||
.section {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.1;
|
||||
margin: 2.4em 0;
|
||||
}
|
||||
|
||||
.section:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.section:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.year {
|
||||
font-size: 2.2em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
line-height: 1.75;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.row:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.date {
|
||||
width: 5.25em;
|
||||
flex-shrink: 0;
|
||||
color: var(--medium);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.section {
|
||||
margin: 1.8em 0;
|
||||
}
|
||||
|
||||
.year {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
@ -1,8 +1,57 @@
|
||||
import { format } from "date-fns";
|
||||
import Link from "../Link/Link";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { NoteType } from "../../types";
|
||||
|
||||
import styles from "./NotesList.module.css";
|
||||
const Section = styled("section", {
|
||||
fontSize: "1.1em",
|
||||
lineHeight: 1.1,
|
||||
margin: "2.4em 0",
|
||||
|
||||
"&:first-of-type": {
|
||||
marginTop: 0,
|
||||
},
|
||||
|
||||
"&:last-of-type": {
|
||||
marginBottom: 0,
|
||||
},
|
||||
|
||||
"@mobile": {
|
||||
margin: "1.8em 0",
|
||||
},
|
||||
});
|
||||
|
||||
const Year = styled("h2", {
|
||||
fontSize: "2.2em",
|
||||
marginTop: 0,
|
||||
marginBottom: "0.5em",
|
||||
|
||||
"@mobile": {
|
||||
fontSize: "2em",
|
||||
},
|
||||
});
|
||||
|
||||
const List = styled("ul", {
|
||||
listStyleType: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
});
|
||||
|
||||
const Post = styled("li", {
|
||||
display: "flex",
|
||||
lineHeight: 1.75,
|
||||
marginBottom: "1em",
|
||||
|
||||
"&:last-of-type": {
|
||||
marginBottom: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const PostDate = styled("span", {
|
||||
width: "5.25em",
|
||||
flexShrink: 0,
|
||||
color: "$medium",
|
||||
});
|
||||
|
||||
export type NotesListProps = {
|
||||
notesByYear: Record<string, NoteType["frontMatter"][]>;
|
||||
@ -13,12 +62,12 @@ const NotesList = ({ notesByYear }: NotesListProps) => {
|
||||
|
||||
Object.entries(notesByYear).forEach(([year, notes]: [string, NoteType["frontMatter"][]]) => {
|
||||
sections.push(
|
||||
<section key={year} className={styles.section}>
|
||||
<h2 className={styles.year}>{year}</h2>
|
||||
<ul className={styles.list}>
|
||||
<Section key={year}>
|
||||
<Year>{year}</Year>
|
||||
<List>
|
||||
{notes.map(({ slug, date, htmlTitle }) => (
|
||||
<li key={slug} className={styles.row}>
|
||||
<span className={styles.date}>{format(new Date(date), "MMM d")}</span>
|
||||
<Post key={slug}>
|
||||
<PostDate>{format(new Date(date), "MMM d")}</PostDate>
|
||||
<span>
|
||||
<Link
|
||||
href={{
|
||||
@ -28,10 +77,10 @@ const NotesList = ({ notesByYear }: NotesListProps) => {
|
||||
dangerouslySetInnerHTML={{ __html: htmlTitle }}
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
</Post>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</List>
|
||||
</Section>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
.link {
|
||||
margin: 0 0.4em;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
vertical-align: -0.2em;
|
||||
}
|
@ -1,16 +1,32 @@
|
||||
import classNames from "classnames";
|
||||
import { OctocatOcticon } from "../Icons";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./OctocatLink.module.css";
|
||||
const Link = styled("a", {
|
||||
margin: "0 0.4em",
|
||||
color: "$text",
|
||||
textDecoration: "none",
|
||||
|
||||
export type OctocatLinkProps = JSX.IntrinsicElements["a"] & {
|
||||
"&:hover": {
|
||||
color: "$link",
|
||||
},
|
||||
});
|
||||
|
||||
const Octocat = styled(OctocatOcticon, {
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
verticalAlign: "-0.2em",
|
||||
fill: "currentColor",
|
||||
});
|
||||
|
||||
export type OctocatLinkProps = ComponentProps<typeof Link> & {
|
||||
repo: string;
|
||||
};
|
||||
|
||||
const OctocatLink = ({ repo, className, ...rest }: OctocatLinkProps) => (
|
||||
<a className={styles.link} href={`https://github.com/${repo}`} target="_blank" rel="noopener noreferrer" {...rest}>
|
||||
<OctocatOcticon fill="currentColor" className={classNames(styles.icon, className)} />
|
||||
</a>
|
||||
<Link href={`https://github.com/${repo}`} target="_blank" rel="noopener noreferrer" {...rest}>
|
||||
<Octocat className={className} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default OctocatLink;
|
||||
|
@ -1,17 +0,0 @@
|
||||
.title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.6em;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
@ -1,22 +1,37 @@
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import classNames from "classnames";
|
||||
import NextLink from "next/link";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import { baseUrl } from "../../lib/config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import styles from "./PageTitle.module.css";
|
||||
const Title = styled("h1", {
|
||||
marginTop: 0,
|
||||
marginBottom: "0.6em",
|
||||
fontSize: "2em",
|
||||
textAlign: "center",
|
||||
|
||||
export type PageTitleProps = JSX.IntrinsicElements["h1"];
|
||||
"@mobile": {
|
||||
fontSize: "1.8em",
|
||||
},
|
||||
});
|
||||
|
||||
const PageTitle = ({ className, children, ...rest }: PageTitleProps) => {
|
||||
const Link = styled("a", {
|
||||
color: "$text",
|
||||
textDecoration: "none",
|
||||
});
|
||||
|
||||
export type PageTitleProps = ComponentProps<typeof Title>;
|
||||
|
||||
const PageTitle = ({ children, ...rest }: PageTitleProps) => {
|
||||
const router = useRouter();
|
||||
const canonical = `${baseUrl}${router.pathname}/`;
|
||||
|
||||
return (
|
||||
<h1 className={classNames(styles.title, className)} {...rest}>
|
||||
<Link href={canonical}>
|
||||
<a className={styles.link}>{children}</a>
|
||||
</Link>
|
||||
</h1>
|
||||
<Title {...rest}>
|
||||
<NextLink href={canonical} passHref={true}>
|
||||
<Link>{children}</Link>
|
||||
</NextLink>
|
||||
</Title>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,62 +0,0 @@
|
||||
.card {
|
||||
width: 100%;
|
||||
padding: 1.2em 1.2em 0.8em 1.2em;
|
||||
border: 1px solid;
|
||||
border-radius: var(--rounded-edge-radius);
|
||||
font-size: 0.85em;
|
||||
color: var(--medium-dark);
|
||||
border-color: var(--kinda-light);
|
||||
|
||||
/* light-dark theme switch fading */
|
||||
transition: border 0.25s ease;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 0.7em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.meta_item {
|
||||
margin-right: 1.5em;
|
||||
font-size: 0.875em;
|
||||
line-height: 2;
|
||||
color: var(--medium);
|
||||
}
|
||||
|
||||
.meta_link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.meta_link:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.octicon,
|
||||
.language_color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.language_color {
|
||||
display: inline-block;
|
||||
width: 1.15em;
|
||||
height: 1.15em;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
vertical-align: text-top;
|
||||
}
|
@ -1,10 +1,71 @@
|
||||
import classNames from "classnames";
|
||||
import { intlFormat, formatDistanceToNowStrict } from "date-fns";
|
||||
import Link from "../Link/Link";
|
||||
import { StarOcticon, ForkOcticon } from "../Icons";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { RepositoryType } from "../../types";
|
||||
|
||||
import styles from "./RepositoryCard.module.css";
|
||||
const Wrapper = styled("div", {
|
||||
width: "100%",
|
||||
padding: "1.2em 1.2em 0.8em 1.2em",
|
||||
border: "1px solid $kindaLight",
|
||||
borderRadius: "$rounded",
|
||||
fontSize: "0.85em",
|
||||
color: "$mediumDark",
|
||||
|
||||
// light-dark theme switch fading
|
||||
transition: "border 0.25s ease",
|
||||
});
|
||||
|
||||
const Name = styled(Link, {
|
||||
fontSize: "1.2em",
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
const Description = styled("p", {
|
||||
marginTop: "0.7em",
|
||||
marginBottom: "0.5em",
|
||||
lineHeight: 1.7,
|
||||
});
|
||||
|
||||
const Meta = styled("div", {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "baseline",
|
||||
});
|
||||
|
||||
const MetaItem = styled("div", {
|
||||
marginRight: "1.5em",
|
||||
fontSize: "0.875em",
|
||||
lineHeight: 2,
|
||||
color: "$medium",
|
||||
});
|
||||
|
||||
const MetaLink = styled("a", {
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
|
||||
"&:hover": {
|
||||
color: "$link",
|
||||
},
|
||||
});
|
||||
|
||||
const MetaIcon = styled("svg", {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
verticalAlign: "text-bottom",
|
||||
marginRight: "0.5em",
|
||||
fill: "currentColor",
|
||||
});
|
||||
|
||||
const LanguageCircle = styled("span", {
|
||||
display: "inline-block",
|
||||
position: "relative",
|
||||
width: "1.15em",
|
||||
height: "1.15em",
|
||||
marginRight: "0.5em",
|
||||
borderRadius: "50%",
|
||||
verticalAlign: "text-top",
|
||||
});
|
||||
|
||||
export type RepositoryCardProps = RepositoryType & {
|
||||
className?: string;
|
||||
@ -20,53 +81,48 @@ const RepositoryCard = ({
|
||||
updatedAt,
|
||||
className,
|
||||
}: RepositoryCardProps) => (
|
||||
<div className={classNames(styles.card, className)}>
|
||||
<Link className={styles.name} href={url}>
|
||||
{name}
|
||||
</Link>
|
||||
<Wrapper className={className}>
|
||||
<Name href={url}>{name}</Name>
|
||||
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
{description && <Description>{description}</Description>}
|
||||
|
||||
<div className={styles.meta}>
|
||||
<Meta>
|
||||
{language && (
|
||||
<div className={styles.meta_item}>
|
||||
<span className={styles.language_color} style={{ backgroundColor: language.color }} />
|
||||
<MetaItem>
|
||||
<LanguageCircle css={{ backgroundColor: language.color }} />
|
||||
<span>{language.name}</span>
|
||||
</div>
|
||||
</MetaItem>
|
||||
)}
|
||||
|
||||
{stars > 0 && (
|
||||
<div className={styles.meta_item}>
|
||||
<a
|
||||
className={styles.meta_link}
|
||||
<MetaItem>
|
||||
<MetaLink
|
||||
href={`${url}/stargazers`}
|
||||
title={`${stars.toLocaleString("en-US")} ${stars === 1 ? "star" : "stars"}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<StarOcticon fill="currentColor" className={styles.octicon} />
|
||||
<MetaIcon as={StarOcticon} />
|
||||
<span>{stars.toLocaleString("en-US")}</span>
|
||||
</a>
|
||||
</div>
|
||||
</MetaLink>
|
||||
</MetaItem>
|
||||
)}
|
||||
|
||||
{forks > 0 && (
|
||||
<div className={styles.meta_item}>
|
||||
<a
|
||||
className={styles.meta_link}
|
||||
<MetaItem>
|
||||
<MetaLink
|
||||
href={`${url}/network/members`}
|
||||
title={`${forks.toLocaleString("en-US")} ${forks === 1 ? "fork" : "forks"}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ForkOcticon fill="currentColor" className={styles.octicon} />
|
||||
<MetaIcon as={ForkOcticon} />
|
||||
<span>{forks.toLocaleString("en-US")}</span>
|
||||
</a>
|
||||
</div>
|
||||
</MetaLink>
|
||||
</MetaItem>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={styles.meta_item}
|
||||
<MetaItem
|
||||
title={intlFormat(
|
||||
new Date(updatedAt),
|
||||
{
|
||||
@ -83,9 +139,9 @@ const RepositoryCard = ({
|
||||
)}
|
||||
>
|
||||
<span>Updated {formatDistanceToNowStrict(new Date(updatedAt), { addSuffix: true })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MetaItem>
|
||||
</Meta>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default RepositoryCard;
|
||||
|
@ -1,48 +0,0 @@
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--medium-dark);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.selfie {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selfie img {
|
||||
border: 1px solid var(--light) !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0 0.6em;
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.selfie {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selfie img {
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
.link:hover .selfie img {
|
||||
border-color: var(--link-underline) !important;
|
||||
}
|
||||
}
|
@ -1,20 +1,66 @@
|
||||
import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import classNames from "classnames";
|
||||
|
||||
import styles from "./Selfie.module.css";
|
||||
import NextLink from "next/link";
|
||||
import NextImage from "next/image";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import selfieJpg from "../../public/static/images/selfie.jpg";
|
||||
|
||||
export type SelfieProps = {
|
||||
className?: string;
|
||||
};
|
||||
const ConstrainImage = styled("div", {
|
||||
width: "50px",
|
||||
height: "50px",
|
||||
lineHeight: 0,
|
||||
padding: 0,
|
||||
|
||||
const Selfie = ({ className }: SelfieProps) => (
|
||||
<Link href="/">
|
||||
<a className={classNames(styles.link, className)}>
|
||||
<div className={styles.selfie}>
|
||||
"@mobile": {
|
||||
width: "70px",
|
||||
height: "70px",
|
||||
},
|
||||
});
|
||||
|
||||
const Image = styled(NextImage, {
|
||||
border: "1px solid $light !important",
|
||||
borderRadius: "50%",
|
||||
|
||||
"@mobile": {
|
||||
borderWidth: "2px !important",
|
||||
},
|
||||
});
|
||||
|
||||
const Link = styled("a", {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
color: "$mediumDark",
|
||||
textDecoration: "none",
|
||||
|
||||
"&:hover": {
|
||||
color: "$link",
|
||||
|
||||
"@mobile": {
|
||||
[`${Image}`]: {
|
||||
borderColor: "$linkUnderline !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Name = styled("span", {
|
||||
margin: "0 0.6em",
|
||||
fontSize: "1.2em",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
|
||||
"@mobile": {
|
||||
display: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export type SelfieProps = ComponentProps<typeof Link>;
|
||||
|
||||
const Selfie = ({ ...rest }: SelfieProps) => (
|
||||
<NextLink href="/" passHref={true}>
|
||||
<Link {...rest}>
|
||||
<ConstrainImage>
|
||||
<Image
|
||||
src={selfieJpg}
|
||||
alt="Photo of Jake Jarvis"
|
||||
@ -24,10 +70,10 @@ const Selfie = ({ className }: SelfieProps) => (
|
||||
layout="intrinsic"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<span className={styles.name}>Jake Jarvis</span>
|
||||
</a>
|
||||
</Link>
|
||||
</ConstrainImage>
|
||||
<Name>Jake Jarvis</Name>
|
||||
</Link>
|
||||
</NextLink>
|
||||
);
|
||||
|
||||
export default memo(Selfie);
|
||||
|
@ -1,28 +0,0 @@
|
||||
.terminal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1em;
|
||||
|
||||
background-color: #000000;
|
||||
color: #cccccc;
|
||||
|
||||
font-size: 0.925em;
|
||||
font-weight: 500;
|
||||
line-height: 2;
|
||||
white-space: pre-wrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.blink {
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
width: 10px;
|
||||
border-bottom: 2px solid #cccccc;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
@ -1,17 +1,46 @@
|
||||
import { forwardRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import type { Ref } from "react";
|
||||
import { keyframes, styled } from "../../lib/styles/stitches.config";
|
||||
import type { Ref, ComponentProps } from "react";
|
||||
|
||||
import styles from "./Terminal.module.css";
|
||||
const BlackBox = styled("div", {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: "1.25em",
|
||||
backgroundColor: "#000000",
|
||||
color: "#cccccc",
|
||||
});
|
||||
|
||||
export type TerminalProps = JSX.IntrinsicElements["div"];
|
||||
const Monospace = styled("pre", {
|
||||
display: "block",
|
||||
margin: 0,
|
||||
lineHeight: 1.75,
|
||||
fontSize: "0.925em",
|
||||
fontWeight: 500,
|
||||
whiteSpace: "pre-wrap",
|
||||
userSelect: "none",
|
||||
});
|
||||
|
||||
// flashing terminal cursor underscore-looking thingy
|
||||
const Underscore = styled("span", {
|
||||
display: "inline-block",
|
||||
verticalAlign: "text-bottom",
|
||||
width: "10px",
|
||||
borderBottom: "2px solid #cccccc",
|
||||
|
||||
// blink every second for 0.4s
|
||||
animation: `${keyframes({ "40%": { opacity: 0 } })} 1s step-end infinite`,
|
||||
});
|
||||
|
||||
export type TerminalProps = ComponentProps<typeof BlackBox>;
|
||||
|
||||
// a DOS-style terminal box with dynamic text
|
||||
const Terminal = forwardRef(function Terminal({ className, ...rest }: TerminalProps, ref: Ref<HTMLSpanElement>) {
|
||||
const Terminal = forwardRef(function Terminal({ ...rest }: TerminalProps, ref: Ref<HTMLSpanElement>) {
|
||||
return (
|
||||
<div className={classNames("monospace", className, styles.terminal)} {...rest}>
|
||||
<span ref={ref} /> <span className={styles.blink} />
|
||||
</div>
|
||||
<BlackBox {...rest}>
|
||||
<Monospace>
|
||||
<span ref={ref} /> <Underscore />
|
||||
</Monospace>
|
||||
</BlackBox>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
.button {
|
||||
border: 0;
|
||||
padding: 0.6em;
|
||||
margin-right: -0.6em;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--medium-dark);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: var(--warning);
|
||||
}
|
@ -1,8 +1,20 @@
|
||||
import { useEffect, useState, memo } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import { SunIcon, MoonIcon } from "../Icons";
|
||||
|
||||
import styles from "./ThemeToggle.module.css";
|
||||
const Button = styled("button", {
|
||||
border: 0,
|
||||
padding: "0.6em",
|
||||
marginRight: "-0.6em",
|
||||
background: "none",
|
||||
cursor: "pointer",
|
||||
color: "$mediumDark",
|
||||
|
||||
"&:hover": {
|
||||
color: "$warning",
|
||||
},
|
||||
});
|
||||
|
||||
export type ThemeToggleProps = {
|
||||
className?: string;
|
||||
@ -16,20 +28,19 @@ const ThemeToggle = ({ className }: ThemeToggleProps) => {
|
||||
useEffect(() => setMounted(true), []);
|
||||
if (!mounted) {
|
||||
return (
|
||||
<button className={styles.button} aria-hidden={true}>
|
||||
<Button aria-hidden={true}>
|
||||
<SunIcon className={className} />
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={styles.button}
|
||||
<Button
|
||||
onClick={() => setTheme(resolvedTheme === "light" ? "dark" : "light")}
|
||||
title={resolvedTheme === "light" ? "Toggle Dark Mode" : "Toggle Light Mode"}
|
||||
>
|
||||
{resolvedTheme === "light" ? <SunIcon className={className} /> : <MoonIcon className={className} />}
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
.display {
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
|
||||
/* fix fuzziness: https://stackoverflow.com/a/13492784 */
|
||||
image-rendering: optimizeSpeed;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
-ms-interpolation-mode: nearest-neighbor;
|
||||
}
|
||||
|
||||
.display div {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.display div canvas {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
}
|
@ -2,8 +2,41 @@ import { useRef, useEffect, useState, memo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import RFB from "@novnc/novnc/core/rfb.js";
|
||||
import Terminal from "../Terminal/Terminal";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
|
||||
import styles from "./VNC.module.css";
|
||||
const Display = styled(
|
||||
"div",
|
||||
{
|
||||
height: "600px",
|
||||
width: "100%",
|
||||
maxWidth: "800px",
|
||||
|
||||
// these are injected by noVNC after connection, so we can't target them directly:
|
||||
"& div": {
|
||||
background: "none !important",
|
||||
|
||||
"& canvas": {
|
||||
cursor: "default !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// fix fuziness in different browsers: https://stackoverflow.com/a/13492784
|
||||
// separate objects since these are duplicate properties: https://github.com/modulz/stitches/issues/758#issuecomment-913580518
|
||||
{
|
||||
imageRendering: "-webkit-optimize-contrast",
|
||||
},
|
||||
{
|
||||
imageRendering: "pixelated",
|
||||
MSInterpolationMode: "nearest-neighbor",
|
||||
}
|
||||
);
|
||||
|
||||
const DOS = styled(Terminal, {
|
||||
height: "400px",
|
||||
width: "100%",
|
||||
maxWidth: "700px",
|
||||
});
|
||||
|
||||
export type VNCProps = {
|
||||
server: string;
|
||||
@ -104,10 +137,10 @@ const VNC = ({ server }: VNCProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Terminal ref={terminalRef} className={styles.terminal} />
|
||||
<DOS ref={terminalRef} />
|
||||
|
||||
{/* the VNC canvas is hidden until we've successfully connected to the socket */}
|
||||
<div ref={screenRef} className={styles.display} style={{ display: "none" }} />
|
||||
<Display ref={screenRef} style={{ display: "none" }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,14 +0,0 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
padding-top: 56.25%;
|
||||
}
|
||||
|
||||
.wrapper > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.wrapper video {
|
||||
border-radius: var(--rounded-edge-radius);
|
||||
}
|
@ -1,8 +1,21 @@
|
||||
import classNames from "classnames";
|
||||
import ReactPlayer from "react-player/file";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { FilePlayerProps } from "react-player/file";
|
||||
|
||||
import styles from "./Video.module.css";
|
||||
const Wrapper = styled("div", {
|
||||
position: "relative",
|
||||
paddingTop: "56.25%",
|
||||
|
||||
"& > div": {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
"& video": {
|
||||
borderRadius: "$rounded",
|
||||
},
|
||||
});
|
||||
|
||||
export type VideoProps = Partial<FilePlayerProps> & {
|
||||
src: {
|
||||
@ -59,7 +72,7 @@ const Video = ({ src, thumbnail, subs, autoplay, className, ...rest }: VideoProp
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.wrapper, className)}>
|
||||
<Wrapper className={className}>
|
||||
<ReactPlayer
|
||||
width="100%"
|
||||
height="100%"
|
||||
@ -69,7 +82,7 @@ const Video = ({ src, thumbnail, subs, autoplay, className, ...rest }: VideoProp
|
||||
config={config}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
.wallpaper {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
background-repeat: repeat;
|
||||
background-position: center;
|
||||
}
|
@ -1,21 +1,43 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { VariantProps } from "@stitches/react";
|
||||
|
||||
import styles from "./Wallpaper.module.css";
|
||||
const Wrapper = styled("main", {
|
||||
width: "100%",
|
||||
minHeight: "400px",
|
||||
|
||||
export type WallpaperProps = JSX.IntrinsicElements["main"] & {
|
||||
variants: {
|
||||
tile: {
|
||||
true: {
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundPosition: "center",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type WallpaperProps = ComponentProps<typeof Wrapper> & {
|
||||
image: string;
|
||||
tile?: boolean;
|
||||
};
|
||||
|
||||
const Wallpaper = ({ image, tile, className, ...rest }: WallpaperProps) => {
|
||||
const bgRef = useRef<HTMLDivElement>(null);
|
||||
const Wallpaper = ({ image, tile, ...rest }: WallpaperProps) => {
|
||||
const bgRef = useRef<VariantProps<typeof Wrapper>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
bgRef.current.style.backgroundImage = `url(${image})`;
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return <main ref={bgRef} className={classNames(styles.wallpaper, tile && styles.tile, className)} {...rest} />;
|
||||
return (
|
||||
<Wrapper
|
||||
tile={tile}
|
||||
// @ts-ignore
|
||||
ref={bgRef}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wallpaper;
|
||||
|
@ -1,15 +0,0 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
padding-top: 56.25%;
|
||||
}
|
||||
|
||||
.wrapper > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.wrapper :global(.react-player__preview),
|
||||
.wrapper iframe {
|
||||
border-radius: var(--rounded-edge-radius);
|
||||
}
|
@ -1,8 +1,21 @@
|
||||
import classNames from "classnames";
|
||||
import ReactPlayer from "react-player/youtube";
|
||||
import { styled } from "../../lib/styles/stitches.config";
|
||||
import type { YouTubePlayerProps } from "react-player/youtube";
|
||||
|
||||
import styles from "./YouTubeEmbed.module.css";
|
||||
const Wrapper = styled("div", {
|
||||
position: "relative",
|
||||
paddingTop: "56.25%",
|
||||
|
||||
"& > div": {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
"& .react-player__preview, & iframe": {
|
||||
borderRadius: "$rounded",
|
||||
},
|
||||
});
|
||||
|
||||
export type YouTubeEmbedProps = Partial<YouTubePlayerProps> & {
|
||||
id: string;
|
||||
@ -10,7 +23,7 @@ export type YouTubeEmbedProps = Partial<YouTubePlayerProps> & {
|
||||
};
|
||||
|
||||
const YouTubeEmbed = ({ id, className, ...rest }: YouTubeEmbedProps) => (
|
||||
<div className={classNames(styles.wrapper, className)}>
|
||||
<Wrapper className={className}>
|
||||
<ReactPlayer
|
||||
width="100%"
|
||||
height="100%"
|
||||
@ -19,7 +32,7 @@ const YouTubeEmbed = ({ id, className, ...rest }: YouTubeEmbedProps) => (
|
||||
controls
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default YouTubeEmbed;
|
||||
|
@ -1,79 +0,0 @@
|
||||
import decamelize from "decamelize";
|
||||
|
||||
// Light/dark theme-related CSS variables that are inlined in Layout.tsx and become available globally.
|
||||
// TODO: Probably invert the object so that *each variable* has a light and dark key.
|
||||
const themes = {
|
||||
light: {
|
||||
backgroundInner: "#ffffff",
|
||||
backgroundOuter: "#fcfcfc",
|
||||
backgroundHeader: "rgba(252, 252, 252, 0.7)",
|
||||
text: "#202020",
|
||||
mediumDark: "#515151",
|
||||
medium: "#5e5e5e",
|
||||
mediumLight: "#757575",
|
||||
light: "#d2d2d2",
|
||||
kindaLight: "#e3e3e3",
|
||||
superLight: "#f4f4f4",
|
||||
superDuperLight: "#fbfbfb",
|
||||
link: "#0e6dc2",
|
||||
linkUnderline: "rgba(14, 109, 194, 0.4)",
|
||||
success: "#44a248",
|
||||
error: "#ff1b1b",
|
||||
warning: "#f78200",
|
||||
|
||||
// Syntax Highlighting (light) - modified from Monokai Light: https://github.com/mlgill/pygments-style-monokailight
|
||||
codeText: "#313131",
|
||||
codeBackground: "#fdfdfd",
|
||||
codeComment: "#656e77",
|
||||
codeKeyword: "#029cb9",
|
||||
codeAttribute: "#70a800",
|
||||
codeNamespace: "#f92672",
|
||||
codeLiteral: "#ae81ff",
|
||||
codePunctuation: "#111111",
|
||||
codeVariable: "#d88200",
|
||||
codeAddition: "#44a248",
|
||||
codeDeletion: "#ff1b1b",
|
||||
},
|
||||
dark: {
|
||||
backgroundInner: "#1e1e1e",
|
||||
backgroundOuter: "#252525",
|
||||
backgroundHeader: "rgba(37, 37, 37, 0.85)",
|
||||
text: "#f1f1f1",
|
||||
mediumDark: "#d7d7d7",
|
||||
medium: "#b1b1b1",
|
||||
mediumLight: "#959595",
|
||||
light: "#646464",
|
||||
kindaLight: "#535353",
|
||||
superLight: "#272727",
|
||||
superDuperLight: "#1f1f1f",
|
||||
link: "#88c7ff",
|
||||
linkUnderline: "rgba(136, 199, 255, 0.4)",
|
||||
success: "#78df55",
|
||||
error: "#ff5151",
|
||||
warning: "#f2b702",
|
||||
|
||||
// Syntax Highlighting (dark) - modified from Dracula: https://github.com/dracula/pygments
|
||||
codeText: "#e4e4e4",
|
||||
codeBackground: "#212121",
|
||||
codeComment: "#929292",
|
||||
codeKeyword: "#3b9dd2",
|
||||
codeAttribute: "#78df55",
|
||||
codeNamespace: "#f95757",
|
||||
codeLiteral: "#d588fb",
|
||||
codePunctuation: "#cccccc",
|
||||
codeVariable: "#fd992a",
|
||||
codeAddition: "#78df55",
|
||||
codeDeletion: "#ff5151",
|
||||
},
|
||||
};
|
||||
|
||||
// JS-friendly camelCase to CSS-friendly kebab-case
|
||||
const camelToKebab = (camel: string) => decamelize(camel, { separator: "-" });
|
||||
|
||||
// converts each variable in a given theme object to CSS syntax and returns all of them as one long string
|
||||
export const toCSS = (theme: Record<string, string>) =>
|
||||
Object.entries(theme)
|
||||
.map(([name, color]) => `--${camelToKebab(name)}:${color};`)
|
||||
.join("");
|
||||
|
||||
export default themes;
|
24
lib/styles/fonts/comic-neue.ts
Normal file
24
lib/styles/fonts/comic-neue.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// Legacy
|
||||
import comicNeueLatin400NormalWoff from "@fontsource/comic-neue/files/comic-neue-latin-400-normal.woff";
|
||||
import comicNeueLatin400NormalWoff2 from "@fontsource/comic-neue/files/comic-neue-latin-400-normal.woff2";
|
||||
import comicNeueLatin700NormalWoff from "@fontsource/comic-neue/files/comic-neue-latin-700-normal.woff";
|
||||
import comicNeueLatin700NormalWoff2 from "@fontsource/comic-neue/files/comic-neue-latin-700-normal.woff2";
|
||||
|
||||
export const ComicNeue = [
|
||||
{
|
||||
fontFamily: "Comic Neue",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: 400,
|
||||
src: `url(${comicNeueLatin400NormalWoff2}) format("woff2"), url(${comicNeueLatin400NormalWoff}) format("woff")`,
|
||||
},
|
||||
{
|
||||
fontFamily: "Comic Neue",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: 700,
|
||||
src: `url(${comicNeueLatin700NormalWoff2}) format("woff2"), url(${comicNeueLatin700NormalWoff}) format("woff")`,
|
||||
},
|
||||
];
|
57
lib/styles/fonts/inter.ts
Normal file
57
lib/styles/fonts/inter.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// Legacy
|
||||
import interLatin400NormalWoff from "@fontsource/inter/files/inter-latin-400-normal.woff";
|
||||
import interLatin400NormalWoff2 from "@fontsource/inter/files/inter-latin-400-normal.woff2";
|
||||
import interLatin500NormalWoff from "@fontsource/inter/files/inter-latin-500-normal.woff";
|
||||
import interLatin500NormalWoff2 from "@fontsource/inter/files/inter-latin-500-normal.woff2";
|
||||
import interLatin700NormalWoff from "@fontsource/inter/files/inter-latin-700-normal.woff";
|
||||
import interLatin700NormalWoff2 from "@fontsource/inter/files/inter-latin-700-normal.woff2";
|
||||
|
||||
// Variable
|
||||
import interLatinVarFullNormalWoff2 from "@fontsource/inter/files/inter-latin-variable-full-normal.woff2";
|
||||
import interLatinExtVarFullNormalWoff2 from "@fontsource/inter/files/inter-latin-ext-variable-full-normal.woff2";
|
||||
|
||||
// re-export hashed URL of the most prominent font so we can preload it
|
||||
export { interLatinVarFullNormalWoff2 as preloadUrl };
|
||||
|
||||
export const Inter = [
|
||||
{
|
||||
fontFamily: "Inter",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: 400,
|
||||
src: `url(${interLatin400NormalWoff2}) format("woff2"), url(${interLatin400NormalWoff}) format("woff")`,
|
||||
},
|
||||
{
|
||||
fontFamily: "Inter",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: 500,
|
||||
src: `url(${interLatin500NormalWoff2}) format("woff2"), url(${interLatin500NormalWoff}) format("woff")`,
|
||||
},
|
||||
{
|
||||
fontFamily: "Inter",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: 700,
|
||||
src: `url(${interLatin700NormalWoff2}) format("woff2"), url(${interLatin700NormalWoff}) format("woff")`,
|
||||
},
|
||||
{
|
||||
fontFamily: "Inter var",
|
||||
fontStyle: "oblique 0deg 10deg",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: "100 900",
|
||||
src: `url(${interLatinVarFullNormalWoff2}) format("woff2")`,
|
||||
unicodeRange:
|
||||
"U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd",
|
||||
},
|
||||
{
|
||||
fontFamily: "Inter var",
|
||||
fontStyle: "oblique 0deg 10deg",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: "100 900",
|
||||
src: `url(${interLatinExtVarFullNormalWoff2}) format("woff2")`,
|
||||
unicodeRange: "U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff",
|
||||
},
|
||||
];
|
76
lib/styles/fonts/roboto-mono.ts
Normal file
76
lib/styles/fonts/roboto-mono.ts
Normal file
@ -0,0 +1,76 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// Legacy
|
||||
import robotoMonoLatin400NormalWoff from "@fontsource/roboto-mono/files/roboto-mono-latin-400-normal.woff";
|
||||
import robotoMonoLatin400NormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-400-normal.woff2";
|
||||
import robotoMonoLatin500NormalWoff from "@fontsource/roboto-mono/files/roboto-mono-latin-500-normal.woff";
|
||||
import robotoMonoLatin500NormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-500-normal.woff2";
|
||||
import robotoMonoLatin700NormalWoff from "@fontsource/roboto-mono/files/roboto-mono-latin-700-normal.woff";
|
||||
import robotoMonoLatin700NormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-700-normal.woff2";
|
||||
|
||||
// Variable
|
||||
import robotoMonoLatinVarWghtOnlyNormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-variable-wghtOnly-normal.woff2";
|
||||
import robotoMonoLatinExtVarWghtOnlyNormalWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-ext-variable-wghtOnly-normal.woff2";
|
||||
import robotoMonoLatinVarWghtOnlyItalicWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-variable-wghtOnly-italic.woff2";
|
||||
import robotoMonoLatinExtVarWghtOnlyItalicWoff2 from "@fontsource/roboto-mono/files/roboto-mono-latin-ext-variable-wghtOnly-italic.woff2";
|
||||
|
||||
// re-export hashed URL of the most prominent font so we can preload it
|
||||
export { robotoMonoLatinVarWghtOnlyNormalWoff2 as preloadUrl };
|
||||
|
||||
export const RobotoMono = [
|
||||
{
|
||||
fontFamily: "Roboto Mono",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: 400,
|
||||
src: `url(${robotoMonoLatin400NormalWoff2}) format("woff2"), url(${robotoMonoLatin400NormalWoff}) format("woff")`,
|
||||
},
|
||||
{
|
||||
fontFamily: "Roboto Mono",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: 500,
|
||||
src: `url(${robotoMonoLatin500NormalWoff2}) format("woff2"), url(${robotoMonoLatin500NormalWoff}) format("woff")`,
|
||||
},
|
||||
{
|
||||
fontFamily: "Roboto Mono",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: 700,
|
||||
src: `url(${robotoMonoLatin700NormalWoff2}) format("woff2"), url(${robotoMonoLatin700NormalWoff}) format("woff")`,
|
||||
},
|
||||
{
|
||||
fontFamily: "Roboto Mono var",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: "100 700",
|
||||
src: `url(${robotoMonoLatinVarWghtOnlyNormalWoff2}) format("woff2")`,
|
||||
unicodeRange:
|
||||
"U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd",
|
||||
},
|
||||
{
|
||||
fontFamily: "Roboto Mono var",
|
||||
fontStyle: "normal",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: "100 700",
|
||||
src: `url(${robotoMonoLatinExtVarWghtOnlyNormalWoff2}) format("woff2")`,
|
||||
unicodeRange: "U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff",
|
||||
},
|
||||
{
|
||||
fontFamily: "Roboto Mono var",
|
||||
fontStyle: "italic",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: "100 700",
|
||||
src: `url(${robotoMonoLatinVarWghtOnlyItalicWoff2}) format("woff2")`,
|
||||
unicodeRange:
|
||||
"U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd",
|
||||
},
|
||||
{
|
||||
fontFamily: "Roboto Mono var",
|
||||
fontStyle: "italic",
|
||||
fontDisplay: "swap",
|
||||
fontWeight: "100 700",
|
||||
src: `url(${robotoMonoLatinExtVarWghtOnlyItalicWoff2}) format("woff2")`,
|
||||
unicodeRange: "U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff",
|
||||
},
|
||||
];
|
11
lib/styles/helpers/hex-to-rgba.ts
Normal file
11
lib/styles/helpers/hex-to-rgba.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// hex -> rgba, pulled from https://github.com/sindresorhus/hex-rgb/blob/main/index.js#L29
|
||||
const hex2rgba = (hex: string, alpha: number) => {
|
||||
const number = Number.parseInt(hex.replace(/^#/, ""), 16);
|
||||
const red = number >> 16;
|
||||
const green = (number >> 8) & 255;
|
||||
const blue = number & 255;
|
||||
|
||||
return `rgba(${red},${green},${blue},${alpha})`;
|
||||
};
|
||||
|
||||
export default hex2rgba;
|
87
lib/styles/helpers/normalize.ts
Normal file
87
lib/styles/helpers/normalize.ts
Normal file
@ -0,0 +1,87 @@
|
||||
// @sindresorhus's modern-normalize.css converted to a JS object, with a bit of cruft removed:
|
||||
// https://github.com/sindresorhus/modern-normalize/blob/b59ec0d3d8654cbb6843bc9ea45aef5f1d680108/modern-normalize.css
|
||||
|
||||
export const normalizeCss = {
|
||||
"*, ::before, ::after": {
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
html: {
|
||||
lineHeight: 1.15,
|
||||
tabSize: 4,
|
||||
WebkitTextSizeAdjust: "100%",
|
||||
},
|
||||
hr: {
|
||||
height: 0,
|
||||
color: "inherit",
|
||||
},
|
||||
"abbr[title]": {
|
||||
textDecoration: "underline dotted",
|
||||
},
|
||||
"b, strong": {
|
||||
fontWeight: "bolder",
|
||||
},
|
||||
small: {
|
||||
fontSize: "80%",
|
||||
},
|
||||
"sub, sup": {
|
||||
fontSize: "75%",
|
||||
lineHeight: 0,
|
||||
position: "relative",
|
||||
verticalAlign: "baseline",
|
||||
},
|
||||
sub: {
|
||||
bottom: "-0.25em",
|
||||
},
|
||||
sup: {
|
||||
top: "-0.5em",
|
||||
},
|
||||
table: {
|
||||
textIndent: 0,
|
||||
borderColor: "inherit",
|
||||
},
|
||||
"button, input, optgroup, select, textarea": {
|
||||
fontFamily: "inherit",
|
||||
fontSize: "100%",
|
||||
lineHeight: 1.15,
|
||||
margin: 0,
|
||||
},
|
||||
"button, select": {
|
||||
textTransform: "none",
|
||||
},
|
||||
"button, [type='button'], [type='reset'], [type='submit']": {
|
||||
WebkitAppearance: "button",
|
||||
},
|
||||
"::-moz-focus-inner": {
|
||||
borderStyle: "none",
|
||||
padding: 0,
|
||||
},
|
||||
":-moz-focusring": {
|
||||
outline: "1px dotted ButtonText",
|
||||
},
|
||||
":-moz-ui-invalid": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
legend: {
|
||||
padding: 0,
|
||||
},
|
||||
progress: {
|
||||
verticalAlign: "baseline",
|
||||
},
|
||||
"::-webkit-inner-spin-button, ::-webkit-outer-spin-button": {
|
||||
height: "auto",
|
||||
},
|
||||
"[type='search']": {
|
||||
WebkitAppearance: "textfield",
|
||||
outlineOffset: "-2px",
|
||||
},
|
||||
"::-webkit-search-decoration": {
|
||||
WebkitAppearance: "none",
|
||||
},
|
||||
"::-webkit-file-upload-button": {
|
||||
WebkitAppearance: "button",
|
||||
font: "inherit",
|
||||
},
|
||||
summary: {
|
||||
display: "list-item",
|
||||
},
|
||||
};
|
184
lib/styles/stitches.config.ts
Normal file
184
lib/styles/stitches.config.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { createStitches, defaultThemeMap } from "@stitches/react";
|
||||
import hex2rgba from "./helpers/hex-to-rgba";
|
||||
|
||||
// modified modern-normalize.css in object form
|
||||
import { normalizeCss } from "./helpers/normalize";
|
||||
|
||||
// web fonts
|
||||
import { Inter, preloadUrl as interPreloadUrl } from "./fonts/inter";
|
||||
import { RobotoMono, preloadUrl as robotoMonoPreloadUrl } from "./fonts/roboto-mono";
|
||||
import { ComicNeue } from "./fonts/comic-neue";
|
||||
|
||||
export const { styled, css, getCssText, globalCss, keyframes, theme, createTheme } = createStitches({
|
||||
theme: {
|
||||
fonts: {
|
||||
sans: `Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`,
|
||||
sansVar: `"Inter var", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`,
|
||||
mono: `"Roboto Mono", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier`,
|
||||
monoVar: `"Roboto Mono var", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier`,
|
||||
},
|
||||
|
||||
colors: {
|
||||
backgroundInner: "#ffffff",
|
||||
backgroundOuter: "#fcfcfc",
|
||||
backgroundHeader: "rgba(252, 252, 252, 0.7)",
|
||||
text: "#202020",
|
||||
mediumDark: "#515151",
|
||||
medium: "#5e5e5e",
|
||||
mediumLight: "#757575",
|
||||
light: "#d2d2d2",
|
||||
kindaLight: "#e3e3e3",
|
||||
superLight: "#f4f4f4",
|
||||
superDuperLight: "#fbfbfb",
|
||||
link: "#0e6dc2",
|
||||
linkUnderline: "rgba(14, 109, 194, 0.4)",
|
||||
success: "#44a248",
|
||||
error: "#ff1b1b",
|
||||
warning: "#f78200",
|
||||
|
||||
// Syntax Highlighting (light) - modified from Monokai Light: https://github.com/mlgill/pygments-style-monokailight
|
||||
codeText: "#313131",
|
||||
codeBackground: "#fdfdfd",
|
||||
codeComment: "#656e77",
|
||||
codeKeyword: "#029cb9",
|
||||
codeAttribute: "#70a800",
|
||||
codeNamespace: "#f92672",
|
||||
codeLiteral: "#ae81ff",
|
||||
codePunctuation: "#111111",
|
||||
codeVariable: "#d88200",
|
||||
codeAddition: "#44a248",
|
||||
codeDeletion: "#ff1b1b",
|
||||
},
|
||||
|
||||
borderWidths: {
|
||||
underline: "calc(0.1em + 0.05rem)",
|
||||
},
|
||||
|
||||
radii: {
|
||||
rounded: "0.65em",
|
||||
},
|
||||
},
|
||||
|
||||
media: {
|
||||
mobile: "(max-width: 768px)",
|
||||
superNarrow: "(max-width: 380px)",
|
||||
},
|
||||
|
||||
utils: {
|
||||
backgroundGradientHack: ({ color = "$linkUnderline" }) => {
|
||||
// allow both pre-set rgba stitches variables and hex values
|
||||
const rgba = color.startsWith("#") ? hex2rgba(color, 0.4) : color;
|
||||
|
||||
return {
|
||||
backgroundImage: `linear-gradient(${rgba}, ${rgba})`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
themeMap: {
|
||||
...defaultThemeMap,
|
||||
backgroundSize: "borderWidths",
|
||||
},
|
||||
});
|
||||
|
||||
export const darkTheme = createTheme({
|
||||
colors: {
|
||||
backgroundInner: "#1e1e1e",
|
||||
backgroundOuter: "#252525",
|
||||
backgroundHeader: "rgba(37, 37, 37, 0.85)",
|
||||
text: "#f1f1f1",
|
||||
mediumDark: "#d7d7d7",
|
||||
medium: "#b1b1b1",
|
||||
mediumLight: "#959595",
|
||||
light: "#646464",
|
||||
kindaLight: "#535353",
|
||||
superLight: "#272727",
|
||||
superDuperLight: "#1f1f1f",
|
||||
link: "#88c7ff",
|
||||
linkUnderline: "rgba(136, 199, 255, 0.4)",
|
||||
success: "#78df55",
|
||||
error: "#ff5151",
|
||||
warning: "#f2b702",
|
||||
|
||||
// Syntax Highlighting (dark) - modified from Dracula: https://github.com/dracula/pygments
|
||||
codeText: "#e4e4e4",
|
||||
codeBackground: "#212121",
|
||||
codeComment: "#929292",
|
||||
codeKeyword: "#3b9dd2",
|
||||
codeAttribute: "#78df55",
|
||||
codeNamespace: "#f95757",
|
||||
codeLiteral: "#d588fb",
|
||||
codePunctuation: "#cccccc",
|
||||
codeVariable: "#fd992a",
|
||||
codeAddition: "#78df55",
|
||||
codeDeletion: "#ff5151",
|
||||
},
|
||||
});
|
||||
|
||||
export const globalStyles = globalCss({
|
||||
// https://github.com/sindresorhus/modern-normalize
|
||||
...normalizeCss,
|
||||
|
||||
// @ts-ignore
|
||||
"@font-face": [...Inter, ...RobotoMono, ...ComicNeue],
|
||||
|
||||
body: {
|
||||
margin: 0,
|
||||
backgroundColor: "$backgroundInner",
|
||||
fontFamily: "$sans",
|
||||
|
||||
// light-dark theme switch fading
|
||||
transition: "background 0.25s ease",
|
||||
},
|
||||
|
||||
"code, kbd, samp, pre": {
|
||||
fontFamily: "$mono",
|
||||
},
|
||||
|
||||
// variable font support
|
||||
"@supports (font-variation-settings: normal)": {
|
||||
body: {
|
||||
fontFamily: "$sansVar",
|
||||
fontOpticalSizing: "auto",
|
||||
},
|
||||
|
||||
"code, kbd, samp, pre": {
|
||||
fontFamily: "$monoVar",
|
||||
},
|
||||
|
||||
// Chrome doesn't automatically slant multi-axis Inter var, for some reason.
|
||||
// Adding "slnt" -10 fixes Chrome but then over-italicizes in Firefox. AHHHHHHHHHH.
|
||||
em: {
|
||||
fontStyle: "normal",
|
||||
fontVariationSettings: `"ital" 1, "slnt" -10`,
|
||||
|
||||
// Roboto Mono doesn't have this problem, but the above fix breaks it, of course.
|
||||
"& code, & kbd, & samp, & pre": {
|
||||
fontStyle: "italic !important",
|
||||
fontVariationSettings: "initial !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// reduced motion preference:
|
||||
// https://web.dev/prefers-reduced-motion/#(bonus)-forcing-reduced-motion-on-all-websites
|
||||
"@media (prefers-reduced-motion: reduce)": {
|
||||
"*, ::before, ::after": {
|
||||
animationDelay: "-1ms !important",
|
||||
animationDuration: "1ms !important",
|
||||
animationIterationCount: "1 !important",
|
||||
backgroundAttachment: "initial !important",
|
||||
scrollBehavior: "auto !important",
|
||||
transitionDuration: "0s !important",
|
||||
transitionDelay: "0s !important",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// re-export hashed URLs of the most important variable fonts so we can preload them in ../../pages/_document.tsx
|
||||
export const preloads = {
|
||||
fonts: {
|
||||
InterVar: interPreloadUrl,
|
||||
RobotoMonoVar: robotoMonoPreloadUrl,
|
||||
},
|
||||
};
|
@ -30,14 +30,23 @@ module.exports = (phase, { defaultConfig }) => {
|
||||
formats: ["image/avif", "image/webp"],
|
||||
minimumCacheTTL: 43200,
|
||||
},
|
||||
experimental: {
|
||||
// use critters to automatically inline critical css:
|
||||
optimizeCss: true,
|
||||
},
|
||||
webpack: (config) => {
|
||||
// this lets us statically import webfonts like we would images, allowing cool things like preloading them
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
issuer: { and: [/\.(js|ts)x?$/] },
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||
issuer: { and: [/\.(js|ts|md)x?$/] },
|
||||
type: "asset/resource",
|
||||
generator: {
|
||||
filename: "static/media/[name].[hash:8][ext]",
|
||||
},
|
||||
});
|
||||
|
||||
// allow processing SVGs from the below packages directly instead of through their different exports, and leave
|
||||
// other static imports of SVGs alone.
|
||||
// see: ./components/Icons/index.ts
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/i,
|
||||
issuer: { and: [/\.(js|ts|md)x?$/] },
|
||||
use: [
|
||||
{
|
||||
loader: "@svgr/webpack",
|
||||
@ -51,9 +60,6 @@ module.exports = (phase, { defaultConfig }) => {
|
||||
},
|
||||
],
|
||||
include: [
|
||||
// allow processing images from these packages directly instead of through their different exports, and leave
|
||||
// other static imports of SVGs alone.
|
||||
// see: ./components/Icons/index.ts
|
||||
path.resolve(__dirname, "node_modules/@primer/octicons/build/svg"),
|
||||
path.resolve(__dirname, "node_modules/feather-icons/dist/icons"),
|
||||
path.resolve(__dirname, "node_modules/simple-icons/icons"),
|
||||
|
30
package.json
30
package.json
@ -17,10 +17,7 @@
|
||||
"dev": "cross-env NODE_OPTIONS='--inspect' next dev",
|
||||
"build": "next build",
|
||||
"analyze": "cross-env ANALYZE=true next build",
|
||||
"lint": "run-s lint:*",
|
||||
"lint:js": "eslint .",
|
||||
"lint:css": "stylelint '**/*.css'",
|
||||
"lint:prettier": "prettier --check ."
|
||||
"lint": "eslint . && prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/comic-neue": "4.5.3",
|
||||
@ -28,17 +25,15 @@
|
||||
"@fontsource/roboto-mono": "4.5.3",
|
||||
"@giscus/react": "^1.1.2",
|
||||
"@hcaptcha/react-hcaptcha": "^1.1.1",
|
||||
"@next/bundle-analyzer": "12.1.1-canary.5",
|
||||
"@next/bundle-analyzer": "12.1.1-canary.6",
|
||||
"@novnc/novnc": "github:novnc/noVNC#679b45fa3b453c7cf32f4b4455f4814818ecf161",
|
||||
"@octokit/graphql": "^4.8.0",
|
||||
"@primer/octicons": "^16.3.1",
|
||||
"@sentry/node": "^6.18.1",
|
||||
"@sentry/tracing": "^6.18.1",
|
||||
"classnames": "^2.3.1",
|
||||
"@stitches/react": "^1.2.7",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"critters": "^0.0.16",
|
||||
"date-fns": "^2.28.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"escape-goat": "^4.0.0",
|
||||
"fathom-client": "^3.4.1",
|
||||
"faunadb": "^4.5.2",
|
||||
@ -48,8 +43,7 @@
|
||||
"gray-matter": "^4.0.3",
|
||||
"is-absolute-url": "^4.0.1",
|
||||
"markdown-to-jsx": "^7.1.6",
|
||||
"modern-normalize": "github:sindresorhus/modern-normalize#b59ec0d3d8654cbb6843bc9ea45aef5f1d680108",
|
||||
"next": "12.1.1-canary.5",
|
||||
"next": "12.1.1-canary.6",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-mdx-remote": "^4.0.0",
|
||||
"next-seo": "^5.1.0",
|
||||
@ -86,7 +80,7 @@
|
||||
"@types/node": "*",
|
||||
"@types/prop-types": "^15.7.4",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.12",
|
||||
"@types/react-dom": "^17.0.13",
|
||||
"@types/react-is": "^17.0.3",
|
||||
"@types/remove-markdown": "^0.3.1",
|
||||
"@types/sanitize-html": "^2.6.2",
|
||||
@ -94,27 +88,19 @@
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "~8.10.0",
|
||||
"eslint-config-next": "12.1.1-canary.5",
|
||||
"eslint-config-prettier": "~8.4.0",
|
||||
"eslint-config-next": "12.1.1-canary.6",
|
||||
"eslint-config-prettier": "~8.5.0",
|
||||
"eslint-plugin-mdx": "~1.16.0",
|
||||
"eslint-plugin-prettier": "~4.0.0",
|
||||
"lint-staged": "^12.3.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.5.1",
|
||||
"simple-git-hooks": "^2.7.0",
|
||||
"stylelint": "~14.5.3",
|
||||
"stylelint-config-prettier": "~9.0.3",
|
||||
"stylelint-prettier": "~2.0.0",
|
||||
"typescript": "^4.6.2"
|
||||
"typescript": "~4.5.5"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npx lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.css": [
|
||||
"stylelint",
|
||||
"prettier --check"
|
||||
],
|
||||
"*.{js,jsx,ts,tsx,md,mdx}": [
|
||||
"eslint",
|
||||
"prettier --check"
|
||||
|
@ -1,33 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { DefaultSeo, SocialProfileJsonLd } from "next-seo";
|
||||
import * as Fathom from "fathom-client";
|
||||
import Layout from "../components/Layout/Layout";
|
||||
import { globalStyles, theme, darkTheme } from "../lib/styles/stitches.config";
|
||||
import * as config from "../lib/config";
|
||||
import { defaultSeo, socialProfileJsonLd } from "../lib/config/seo";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
import type { NextPage } from "next";
|
||||
import type { AppProps as NextAppProps } from "next/app";
|
||||
|
||||
// global webfonts -- imported here so they're processed through PostCSS
|
||||
import "@fontsource/inter/latin-400.css";
|
||||
import "@fontsource/inter/latin-500.css";
|
||||
import "@fontsource/inter/latin-700.css";
|
||||
import "@fontsource/inter/variable-full.css";
|
||||
import "@fontsource/roboto-mono/latin-400.css";
|
||||
import "@fontsource/roboto-mono/latin-500.css";
|
||||
import "@fontsource/roboto-mono/latin-700.css";
|
||||
import "@fontsource/roboto-mono/variable.css";
|
||||
import "@fontsource/roboto-mono/variable-italic.css";
|
||||
|
||||
// global styles
|
||||
import "modern-normalize/modern-normalize.css";
|
||||
import "../styles/settings.css";
|
||||
import "../styles/typography.css";
|
||||
import "../styles/index.css";
|
||||
|
||||
// https://nextjs.org/docs/basic-features/layouts#with-typescript
|
||||
export type AppProps = NextAppProps & {
|
||||
Component: NextPage & {
|
||||
@ -68,28 +51,12 @@ const App = ({ Component, pageProps }: AppProps) => {
|
||||
// allow layout overrides per-page, but default to plain `<Layout />`
|
||||
const getLayout = Component.getLayout || ((page) => <Layout>{page}</Layout>);
|
||||
|
||||
// inject body styles defined in ../lib/styles/stitches.config.ts
|
||||
globalStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* static asset preloads */}
|
||||
<Head>
|
||||
{/* TODO: these hrefs will change (unpredictably?) at some point. find a safer way to get them from webpack. */}
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href="/_next/static/media/inter-latin-variable-full-normal.79d31200.woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href="/_next/static/media/roboto-mono-latin-variable-wghtOnly-normal.3689861c.woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
{/* all SEO config is in ../lib/seo.ts except for canonical URLs, which require access to next router */}
|
||||
{/* all SEO config is in ../lib/config/seo.ts except for canonical URLs, which require access to next router */}
|
||||
<DefaultSeo
|
||||
{...defaultSeo}
|
||||
canonical={canonical}
|
||||
@ -103,8 +70,16 @@ const App = ({ Component, pageProps }: AppProps) => {
|
||||
/>
|
||||
<SocialProfileJsonLd {...socialProfileJsonLd} />
|
||||
|
||||
{/* NOTE: this *must* come last in this fragment */}
|
||||
<ThemeProvider>{getLayout(<Component {...pageProps} />)}</ThemeProvider>
|
||||
<ThemeProvider
|
||||
// theme classnames are generated dynamically by stitches, so have ThemeProvider pull them from there
|
||||
attribute="class"
|
||||
value={{
|
||||
light: theme.className,
|
||||
dark: darkTheme.className,
|
||||
}}
|
||||
>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Html, Head, Main, NextScript } from "next/document";
|
||||
import themes, { toCSS } from "../lib/config/themes";
|
||||
import { getCssText, preloads } from "../lib/styles/stitches.config";
|
||||
import * as config from "../lib/config";
|
||||
|
||||
// https://nextjs.org/docs/advanced-features/custom-document
|
||||
@ -7,13 +7,12 @@ const Document = () => {
|
||||
return (
|
||||
<Html lang={config.siteLocale?.replace("_", "-")}>
|
||||
<Head>
|
||||
{/* convert themes object into inlined css variables */}
|
||||
<style
|
||||
id="theme-colors"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `:root{${toCSS(themes.light)}}[data-theme="dark"]{${toCSS(themes.dark)}}`,
|
||||
}}
|
||||
/>
|
||||
{/* static asset preloads */}
|
||||
<link rel="preload" as="font" type="font/woff2" href={preloads.fonts.InterVar} crossOrigin="anonymous" />
|
||||
<link rel="preload" as="font" type="font/woff2" href={preloads.fonts.RobotoMonoVar} crossOrigin="anonymous" />
|
||||
|
||||
{/* stitches SSR: https://stitches.dev/blog/using-nextjs-with-stitches#step-3-ssr */}
|
||||
<style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
|
@ -3,6 +3,18 @@ import Content from "../components/Content/Content";
|
||||
import PageTitle from "../components/PageTitle/PageTitle";
|
||||
import Link from "../components/Link/Link";
|
||||
import ContactForm from "../components/ContactForm/ContactForm";
|
||||
import { styled } from "../lib/styles/stitches.config";
|
||||
|
||||
const Wrapper = styled(Content, {
|
||||
maxWidth: "600px",
|
||||
margin: "0 auto",
|
||||
});
|
||||
|
||||
const PubKey = styled("code", {
|
||||
fontSize: "0.925em",
|
||||
wordSpacing: "-0.175em",
|
||||
whiteSpace: "normal",
|
||||
});
|
||||
|
||||
const Contact = () => (
|
||||
<>
|
||||
@ -15,38 +27,23 @@ const Contact = () => (
|
||||
|
||||
<PageTitle>📬 Contact Me</PageTitle>
|
||||
|
||||
<Content>
|
||||
<div className="wrapper">
|
||||
<p>
|
||||
Fill out this quick form and I'll get back to you as soon as I can! You can also{" "}
|
||||
<Link href="mailto:jake@jarv.is">email me directly</Link>, send me a{" "}
|
||||
<Link href="https://twitter.com/messages/compose?recipient_id=229769022">direct message on Twitter</Link>, or{" "}
|
||||
<Link href="sms:+1-617-917-3737">text me</Link>.
|
||||
</p>
|
||||
<p>
|
||||
🔐 You can grab my public key here:{" "}
|
||||
<Link href="/pubkey.asc" title="My Public PGP Key" rel="pgpkey authn noopener" forceNewWindow>
|
||||
<code className="pubkey">6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</code>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<Wrapper>
|
||||
<p>
|
||||
Fill out this quick form and I'll get back to you as soon as I can! You can also{" "}
|
||||
<Link href="mailto:jake@jarv.is">email me directly</Link>, send me a{" "}
|
||||
<Link href="https://twitter.com/messages/compose?recipient_id=229769022">direct message on Twitter</Link>, or{" "}
|
||||
<Link href="sms:+1-617-917-3737">text me</Link>.
|
||||
</p>
|
||||
<p>
|
||||
🔐 You can grab my public key here:{" "}
|
||||
<Link href="/pubkey.asc" title="My Public PGP Key" rel="pgpkey authn noopener" forceNewWindow>
|
||||
<PubKey>6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</PubKey>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<ContactForm />
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
<style jsx>{`
|
||||
.wrapper {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pubkey {
|
||||
font-size: 0.925em;
|
||||
word-spacing: -0.175em;
|
||||
white-space: normal;
|
||||
}
|
||||
`}</style>
|
||||
<ContactForm />
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -3,9 +3,18 @@ import Content from "../components/Content/Content";
|
||||
import PageTitle from "../components/PageTitle/PageTitle";
|
||||
import Link from "../components/Link/Link";
|
||||
import Video from "../components/Video/Video";
|
||||
import { styled } from "../lib/styles/stitches.config";
|
||||
|
||||
import thumbnail from "../public/static/images/hillary/thumb.png";
|
||||
|
||||
const Copyright = styled("p", {
|
||||
textAlign: "center",
|
||||
fontSize: "0.9em",
|
||||
lineHeight: 1.8,
|
||||
margin: "1.25em 1em 0 1em",
|
||||
color: "$mediumLight",
|
||||
});
|
||||
|
||||
const Hillary = () => (
|
||||
<>
|
||||
<NextSeo
|
||||
@ -27,7 +36,7 @@ const Hillary = () => (
|
||||
subs="/static/images/hillary/subs.en.vtt"
|
||||
/>
|
||||
|
||||
<p className="copyright">
|
||||
<Copyright>
|
||||
Video is property of{" "}
|
||||
<Link href="https://www.hillaryclinton.com/" style={{ fontWeight: 700 }}>
|
||||
Hillary for America
|
||||
@ -41,18 +50,8 @@ const Hillary = () => (
|
||||
CNN / WarnerMedia
|
||||
</Link>
|
||||
. © 2016.
|
||||
</p>
|
||||
</Copyright>
|
||||
</Content>
|
||||
|
||||
<style jsx>{`
|
||||
.copyright {
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.8;
|
||||
margin: 1.25em 1em 0 1em;
|
||||
color: var(--medium-light);
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
|
||||
|
704
pages/index.tsx
704
pages/index.tsx
@ -1,366 +1,362 @@
|
||||
import Content from "../components/Content/Content";
|
||||
import ColorfulLink from "../components/ColorfulLink/ColorfulLink";
|
||||
import Link, { CustomLinkProps } from "../components/Link/Link";
|
||||
import { styled, keyframes, darkTheme } from "../lib/styles/stitches.config";
|
||||
|
||||
const Wrapper = styled(Content, {
|
||||
fontSize: "1em",
|
||||
lineHeight: 1,
|
||||
});
|
||||
|
||||
const ColorfulLink = ({
|
||||
lightColor,
|
||||
darkColor,
|
||||
...rest
|
||||
}: CustomLinkProps & {
|
||||
lightColor: string;
|
||||
darkColor: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
css={{
|
||||
color: lightColor,
|
||||
backgroundGradientHack: { color: lightColor },
|
||||
|
||||
[`.${darkTheme} &`]: {
|
||||
color: darkColor,
|
||||
backgroundGradientHack: { color: darkColor },
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const H1 = styled("h1", {
|
||||
margin: "0 0 0.5em -0.03em",
|
||||
fontSize: "1.8em",
|
||||
fontWeight: 500,
|
||||
letterSpacing: "-0.01em",
|
||||
|
||||
"@mobile": {
|
||||
fontSize: "1.5em",
|
||||
},
|
||||
});
|
||||
|
||||
const H2 = styled("h2", {
|
||||
margin: "0.5em 0 0.5em -0.03em",
|
||||
fontSize: "1.35em",
|
||||
fontWeight: 400,
|
||||
letterSpacing: "-0.016em",
|
||||
lineHeight: 1.4,
|
||||
|
||||
"@mobile": {
|
||||
fontSize: "1.2em",
|
||||
},
|
||||
});
|
||||
|
||||
const Paragraph = styled("p", {
|
||||
margin: "0.85em 0",
|
||||
letterSpacing: "-0.004em",
|
||||
lineHeight: 1.7,
|
||||
|
||||
"&:last-of-type": {
|
||||
marginBottom: 0,
|
||||
},
|
||||
|
||||
"@mobile": {
|
||||
fontSize: "0.925em",
|
||||
},
|
||||
});
|
||||
|
||||
const hello = keyframes({
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"5%": { transform: "rotate(14deg)" },
|
||||
"10%": { transform: "rotate(-8deg)" },
|
||||
"15%": { transform: "rotate(14deg)" },
|
||||
"20%": { transform: "rotate(-4deg)" },
|
||||
"25%": { transform: "rotate(10deg)" },
|
||||
"30%": { transform: "rotate(0deg)" },
|
||||
// pause for ~9 out of 10 seconds
|
||||
"100%": { transform: "rotate(0deg)" },
|
||||
});
|
||||
|
||||
const Wave = styled("span", {
|
||||
display: "inline-block",
|
||||
marginLeft: "0.1em",
|
||||
fontSize: "1.2em",
|
||||
animation: `${hello} 5s infinite`,
|
||||
animationDelay: "1s",
|
||||
transformOrigin: "65% 80%",
|
||||
willChange: "transform",
|
||||
});
|
||||
|
||||
const PGPKey = styled("sup", {
|
||||
margin: "0 0.15em",
|
||||
fontSize: "0.65em",
|
||||
wordSpacing: "-0.3em",
|
||||
});
|
||||
|
||||
const Quiet = styled("span", {
|
||||
color: "$mediumLight",
|
||||
});
|
||||
|
||||
const EasterEgg = styled(ColorfulLink, {
|
||||
cursor: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>") 5 5, auto`,
|
||||
});
|
||||
|
||||
const Index = () => (
|
||||
<>
|
||||
<Content>
|
||||
<div>
|
||||
<h1>
|
||||
Hi there! I'm Jake. <span className="wave">👋</span>
|
||||
</h1>
|
||||
<Wrapper>
|
||||
<H1>
|
||||
Hi there! I'm Jake. <Wave>👋</Wave>
|
||||
</H1>
|
||||
|
||||
<h2>
|
||||
I'm a frontend web developer based in{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&iv_load_policy=3"
|
||||
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
|
||||
lightColor="#fb4d42"
|
||||
darkColor="#ff5146"
|
||||
>
|
||||
Boston
|
||||
</ColorfulLink>
|
||||
.
|
||||
</h2>
|
||||
<H2>
|
||||
I'm a frontend web developer based in{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&iv_load_policy=3"
|
||||
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
|
||||
lightColor="#fb4d42"
|
||||
darkColor="#ff5146"
|
||||
>
|
||||
Boston
|
||||
</ColorfulLink>
|
||||
.
|
||||
</H2>
|
||||
|
||||
<p>
|
||||
I specialize in{" "}
|
||||
<ColorfulLink
|
||||
href="https://stackoverflow.blog/2018/01/11/brutal-lifecycle-javascript-frameworks/"
|
||||
title='"The Brutal Lifecycle of JavaScript Frameworks" by Ian Allen'
|
||||
lightColor="#1091b3"
|
||||
darkColor="#6fcbe3"
|
||||
>
|
||||
modern JS frameworks
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
href="http://vanilla-js.com/"
|
||||
title="The best JS framework in the world by Eric Wastl"
|
||||
lightColor="#f48024"
|
||||
darkColor="#e18431"
|
||||
>
|
||||
vanilla JavaScript
|
||||
</ColorfulLink>{" "}
|
||||
to make nifty{" "}
|
||||
<ColorfulLink href="https://jamstack.wtf/" title="WTF is JAMstack?" lightColor="#04a699" darkColor="#08bbac">
|
||||
JAMstack sites
|
||||
</ColorfulLink>{" "}
|
||||
with dynamic{" "}
|
||||
<ColorfulLink
|
||||
href="https://nodejs.org/en/"
|
||||
title="Node.js Official Website"
|
||||
lightColor="#6fbc4e"
|
||||
darkColor="#84d95f"
|
||||
>
|
||||
Node.js
|
||||
</ColorfulLink>{" "}
|
||||
services. But I'm fluent in non-buzzwords like{" "}
|
||||
<ColorfulLink
|
||||
href="https://stitcher.io/blog/php-in-2020"
|
||||
title='"PHP in 2020" by Brent Roose'
|
||||
lightColor="#8892bf"
|
||||
darkColor="#a4afe3"
|
||||
>
|
||||
PHP
|
||||
</ColorfulLink>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.ruby-lang.org/en/"
|
||||
title="Ruby Official Website"
|
||||
lightColor="#d34135"
|
||||
darkColor="#f95a4d"
|
||||
>
|
||||
Ruby
|
||||
</ColorfulLink>
|
||||
, and{" "}
|
||||
<ColorfulLink
|
||||
href="https://golang.org/"
|
||||
title="Golang Official Website"
|
||||
lightColor="#00acd7"
|
||||
darkColor="#2ad1fb"
|
||||
>
|
||||
Go
|
||||
</ColorfulLink>{" "}
|
||||
too.
|
||||
</p>
|
||||
<Paragraph>
|
||||
I specialize in{" "}
|
||||
<ColorfulLink
|
||||
href="https://stackoverflow.blog/2018/01/11/brutal-lifecycle-javascript-frameworks/"
|
||||
title='"The Brutal Lifecycle of JavaScript Frameworks" by Ian Allen'
|
||||
lightColor="#1091b3"
|
||||
darkColor="#6fcbe3"
|
||||
>
|
||||
modern JS frameworks
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
href="http://vanilla-js.com/"
|
||||
title="The best JS framework in the world by Eric Wastl"
|
||||
lightColor="#f48024"
|
||||
darkColor="#e18431"
|
||||
>
|
||||
vanilla JavaScript
|
||||
</ColorfulLink>{" "}
|
||||
to make nifty{" "}
|
||||
<ColorfulLink href="https://jamstack.wtf/" title="WTF is JAMstack?" lightColor="#04a699" darkColor="#08bbac">
|
||||
JAMstack sites
|
||||
</ColorfulLink>{" "}
|
||||
with dynamic{" "}
|
||||
<ColorfulLink
|
||||
href="https://nodejs.org/en/"
|
||||
title="Node.js Official Website"
|
||||
lightColor="#6fbc4e"
|
||||
darkColor="#84d95f"
|
||||
>
|
||||
Node.js
|
||||
</ColorfulLink>{" "}
|
||||
services. But I'm fluent in non-buzzwords like{" "}
|
||||
<ColorfulLink
|
||||
href="https://stitcher.io/blog/php-in-2020"
|
||||
title='"PHP in 2020" by Brent Roose'
|
||||
lightColor="#8892bf"
|
||||
darkColor="#a4afe3"
|
||||
>
|
||||
PHP
|
||||
</ColorfulLink>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.ruby-lang.org/en/"
|
||||
title="Ruby Official Website"
|
||||
lightColor="#d34135"
|
||||
darkColor="#f95a4d"
|
||||
>
|
||||
Ruby
|
||||
</ColorfulLink>
|
||||
, and{" "}
|
||||
<ColorfulLink href="https://golang.org/" title="Golang Official Website" lightColor="#00acd7" darkColor="#2ad1fb">
|
||||
Go
|
||||
</ColorfulLink>{" "}
|
||||
too.
|
||||
</Paragraph>
|
||||
|
||||
<p>
|
||||
Whenever possible, I also apply my experience in{" "}
|
||||
<ColorfulLink
|
||||
href="https://github.com/jakejarvis/awesome-shodan-queries"
|
||||
title="jakejarvis/awesome-shodan-queries on GitHub"
|
||||
lightColor="#00b81a"
|
||||
darkColor="#57f06d"
|
||||
>
|
||||
application security
|
||||
</ColorfulLink>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
|
||||
title='"What is serverless computing?" on Cloudflare'
|
||||
lightColor="#0098ec"
|
||||
darkColor="#43b9fb"
|
||||
>
|
||||
serverless stacks
|
||||
</ColorfulLink>
|
||||
, and{" "}
|
||||
<ColorfulLink
|
||||
href="https://xkcd.com/1319/"
|
||||
title='"Automation" on xkcd'
|
||||
lightColor="#ff6200"
|
||||
darkColor="#f46c16"
|
||||
>
|
||||
DevOps automation
|
||||
</ColorfulLink>
|
||||
.
|
||||
</p>
|
||||
<Paragraph>
|
||||
Whenever possible, I also apply my experience in{" "}
|
||||
<ColorfulLink
|
||||
href="https://github.com/jakejarvis/awesome-shodan-queries"
|
||||
title="jakejarvis/awesome-shodan-queries on GitHub"
|
||||
lightColor="#00b81a"
|
||||
darkColor="#57f06d"
|
||||
>
|
||||
application security
|
||||
</ColorfulLink>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
|
||||
title='"What is serverless computing?" on Cloudflare'
|
||||
lightColor="#0098ec"
|
||||
darkColor="#43b9fb"
|
||||
>
|
||||
serverless stacks
|
||||
</ColorfulLink>
|
||||
, and{" "}
|
||||
<ColorfulLink href="https://xkcd.com/1319/" title='"Automation" on xkcd' lightColor="#ff6200" darkColor="#f46c16">
|
||||
DevOps automation
|
||||
</ColorfulLink>
|
||||
.
|
||||
</Paragraph>
|
||||
|
||||
<p>
|
||||
I fell in love with{" "}
|
||||
<ColorfulLink
|
||||
href="/previously/"
|
||||
title="My Terrible, Horrible, No Good, Very Bad First Websites"
|
||||
lightColor="#4169e1"
|
||||
darkColor="#8ca9ff"
|
||||
>
|
||||
frontend web design
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
href="/notes/my-first-code/"
|
||||
title="Jake's Bulletin Board, circa 2003"
|
||||
lightColor="#9932cc"
|
||||
darkColor="#d588fb"
|
||||
>
|
||||
backend programming
|
||||
</ColorfulLink>{" "}
|
||||
back when my only source of income was{" "}
|
||||
<span className="birthday">
|
||||
<ColorfulLink
|
||||
href="/birthday/"
|
||||
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
||||
lightColor="#e40088"
|
||||
darkColor="#fd40b1"
|
||||
>
|
||||
the Tooth Fairy
|
||||
</ColorfulLink>
|
||||
</span>
|
||||
. <span className="quiet">I've improved a bit since then, I think...</span>
|
||||
</p>
|
||||
<Paragraph>
|
||||
I fell in love with{" "}
|
||||
<ColorfulLink
|
||||
href="/previously/"
|
||||
title="My Terrible, Horrible, No Good, Very Bad First Websites"
|
||||
lightColor="#4169e1"
|
||||
darkColor="#8ca9ff"
|
||||
>
|
||||
frontend web design
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
href="/notes/my-first-code/"
|
||||
title="Jake's Bulletin Board, circa 2003"
|
||||
lightColor="#9932cc"
|
||||
darkColor="#d588fb"
|
||||
>
|
||||
backend programming
|
||||
</ColorfulLink>{" "}
|
||||
back when my only source of income was{" "}
|
||||
<EasterEgg
|
||||
href="/birthday/"
|
||||
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
||||
lightColor="#e40088"
|
||||
darkColor="#fd40b1"
|
||||
>
|
||||
the Tooth Fairy
|
||||
</EasterEgg>
|
||||
. <Quiet>I've improved a bit since then, I think...</Quiet>
|
||||
</Paragraph>
|
||||
|
||||
<p>
|
||||
Over the years, some of my side projects{" "}
|
||||
<ColorfulLink
|
||||
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
|
||||
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
|
||||
lightColor="#ff1b1b"
|
||||
darkColor="#f06060"
|
||||
>
|
||||
have
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="/leo/"
|
||||
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
|
||||
lightColor="#f78200"
|
||||
darkColor="#fd992a"
|
||||
>
|
||||
been
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
|
||||
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
|
||||
lightColor="#f2b702"
|
||||
darkColor="#ffcc2e"
|
||||
>
|
||||
featured
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
|
||||
title='"The new Facebook is on a roll" on CNN Money'
|
||||
lightColor="#5ebd3e"
|
||||
darkColor="#78df55"
|
||||
>
|
||||
by
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.wired.com/2007/04/our-web-servers/"
|
||||
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
|
||||
lightColor="#009cdf"
|
||||
darkColor="#29bfff"
|
||||
>
|
||||
various
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
|
||||
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
|
||||
lightColor="#3e49bb"
|
||||
darkColor="#7b87ff"
|
||||
>
|
||||
media
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
|
||||
title='"Your Next Client? The CEO's Son" on Advertising Age'
|
||||
lightColor="#973999"
|
||||
darkColor="#db60dd"
|
||||
>
|
||||
outlets
|
||||
</ColorfulLink>
|
||||
.
|
||||
</p>
|
||||
<Paragraph>
|
||||
Over the years, some of my side projects{" "}
|
||||
<ColorfulLink
|
||||
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
|
||||
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
|
||||
lightColor="#ff1b1b"
|
||||
darkColor="#f06060"
|
||||
>
|
||||
have
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="/leo/"
|
||||
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
|
||||
lightColor="#f78200"
|
||||
darkColor="#fd992a"
|
||||
>
|
||||
been
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
|
||||
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
|
||||
lightColor="#f2b702"
|
||||
darkColor="#ffcc2e"
|
||||
>
|
||||
featured
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
|
||||
title='"The new Facebook is on a roll" on CNN Money'
|
||||
lightColor="#5ebd3e"
|
||||
darkColor="#78df55"
|
||||
>
|
||||
by
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.wired.com/2007/04/our-web-servers/"
|
||||
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
|
||||
lightColor="#009cdf"
|
||||
darkColor="#29bfff"
|
||||
>
|
||||
various
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
|
||||
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
|
||||
lightColor="#3e49bb"
|
||||
darkColor="#7b87ff"
|
||||
>
|
||||
media
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
|
||||
title='"Your Next Client? The CEO's Son" on Advertising Age'
|
||||
lightColor="#973999"
|
||||
darkColor="#db60dd"
|
||||
>
|
||||
outlets
|
||||
</ColorfulLink>
|
||||
.
|
||||
</Paragraph>
|
||||
|
||||
<p>
|
||||
You can find more of my work on{" "}
|
||||
<ColorfulLink
|
||||
href="https://github.com/jakejarvis"
|
||||
title="Jake Jarvis on GitHub"
|
||||
lightColor="#8d4eff"
|
||||
darkColor="#a379f0"
|
||||
>
|
||||
GitHub
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.linkedin.com/in/jakejarvis/"
|
||||
title="Jake Jarvis on LinkedIn"
|
||||
lightColor="#0073b1"
|
||||
darkColor="#3b9dd2"
|
||||
>
|
||||
LinkedIn
|
||||
</ColorfulLink>
|
||||
. I'm always available to connect over{" "}
|
||||
<ColorfulLink href="/contact/" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
|
||||
email
|
||||
</ColorfulLink>{" "}
|
||||
<sup className="monospace pgp_key">
|
||||
<ColorfulLink
|
||||
href="/pubkey.asc"
|
||||
rel="pgpkey authn noopener"
|
||||
title="My Public Key"
|
||||
lightColor="#757575"
|
||||
darkColor="#959595"
|
||||
style={{ background: "none" }}
|
||||
forceNewWindow
|
||||
>
|
||||
🔐 2B0C 9CF2 51E6 9A39
|
||||
</ColorfulLink>
|
||||
</sup>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://twitter.com/jakejarvis"
|
||||
title="Jake Jarvis on Twitter"
|
||||
lightColor="#00acee"
|
||||
darkColor="#3bc9ff"
|
||||
>
|
||||
Twitter
|
||||
</ColorfulLink>
|
||||
, or{" "}
|
||||
<ColorfulLink
|
||||
href="sms:+1-617-917-3737"
|
||||
title="Send SMS to +1 (617) 917-3737"
|
||||
lightColor="#6fcc01"
|
||||
darkColor="#8edb34"
|
||||
>
|
||||
SMS
|
||||
</ColorfulLink>{" "}
|
||||
as well!
|
||||
</p>
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
<style jsx>{`
|
||||
div {
|
||||
font-size: 1.1em;
|
||||
line-height: 1;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 0.5em -0.03em;
|
||||
font-size: 1.8em;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
h2 {
|
||||
margin: 0.5em 0 0.5em -0.03em;
|
||||
font-size: 1.35em;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.016em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
p {
|
||||
margin: 0.85em 0;
|
||||
letter-spacing: -0.004em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.wave {
|
||||
display: inline-block;
|
||||
margin-left: 0.1em;
|
||||
font-size: 1.2em;
|
||||
animation: wave 5s infinite;
|
||||
animation-delay: 1s;
|
||||
transform-origin: 65% 80%;
|
||||
will-change: transform;
|
||||
}
|
||||
.pgp_key {
|
||||
margin: 0 0.15em;
|
||||
font-size: 0.65em;
|
||||
word-spacing: -0.3em;
|
||||
}
|
||||
.quiet {
|
||||
color: var(--medium-light);
|
||||
}
|
||||
.birthday :global(a:hover) {
|
||||
/* magic wand cursor easter egg */
|
||||
cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>")
|
||||
5 5,
|
||||
auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
p {
|
||||
font-size: 0.925em;
|
||||
}
|
||||
}
|
||||
|
||||
/* https://jarv.is/notes/css-waving-hand-emoji/ */
|
||||
@keyframes wave {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
5% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
10% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
15% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
// pause for 3.5 out of 5 seconds
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
<Paragraph>
|
||||
You can find more of my work on{" "}
|
||||
<ColorfulLink
|
||||
href="https://github.com/jakejarvis"
|
||||
title="Jake Jarvis on GitHub"
|
||||
lightColor="#8d4eff"
|
||||
darkColor="#a379f0"
|
||||
>
|
||||
GitHub
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.linkedin.com/in/jakejarvis/"
|
||||
title="Jake Jarvis on LinkedIn"
|
||||
lightColor="#0073b1"
|
||||
darkColor="#3b9dd2"
|
||||
>
|
||||
LinkedIn
|
||||
</ColorfulLink>
|
||||
. I'm always available to connect over{" "}
|
||||
<ColorfulLink href="/contact/" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
|
||||
email
|
||||
</ColorfulLink>{" "}
|
||||
<PGPKey>
|
||||
<ColorfulLink
|
||||
href="/pubkey.asc"
|
||||
rel="pgpkey authn noopener"
|
||||
title="My Public Key"
|
||||
lightColor="#757575"
|
||||
darkColor="#959595"
|
||||
style={{ background: "none" }}
|
||||
forceNewWindow
|
||||
>
|
||||
<code>🔐 2B0C 9CF2 51E6 9A39</code>
|
||||
</ColorfulLink>
|
||||
</PGPKey>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://twitter.com/jakejarvis"
|
||||
title="Jake Jarvis on Twitter"
|
||||
lightColor="#00acee"
|
||||
darkColor="#3bc9ff"
|
||||
>
|
||||
Twitter
|
||||
</ColorfulLink>
|
||||
, or{" "}
|
||||
<ColorfulLink
|
||||
href="sms:+1-617-917-3737"
|
||||
title="Send SMS to +1 (617) 917-3737"
|
||||
lightColor="#6fcc01"
|
||||
darkColor="#8edb34"
|
||||
>
|
||||
SMS
|
||||
</ColorfulLink>{" "}
|
||||
as well!
|
||||
</Paragraph>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default Index;
|
||||
|
@ -3,9 +3,18 @@ import Content from "../components/Content/Content";
|
||||
import PageTitle from "../components/PageTitle/PageTitle";
|
||||
import Link from "../components/Link/Link";
|
||||
import Video from "../components/Video/Video";
|
||||
import { styled } from "../lib/styles/stitches.config";
|
||||
|
||||
import thumbnail from "../public/static/images/leo/thumb.png";
|
||||
|
||||
const Copyright = styled("p", {
|
||||
textAlign: "center",
|
||||
fontSize: "0.9em",
|
||||
lineHeight: 1.8,
|
||||
margin: "1.25em 1em 0 1em",
|
||||
color: "$mediumLight",
|
||||
});
|
||||
|
||||
const Leo = () => (
|
||||
<>
|
||||
<NextSeo
|
||||
@ -28,7 +37,7 @@ const Leo = () => (
|
||||
subs="/static/images/leo/subs.en.vtt"
|
||||
/>
|
||||
|
||||
<p className="copyright">
|
||||
<Copyright>
|
||||
Video is property of{" "}
|
||||
<Link href="https://web.archive.org/web/20070511004304/http://www.g4techtv.ca/" style={{ fontWeight: 700 }}>
|
||||
G4techTV Canada
|
||||
@ -38,18 +47,8 @@ const Leo = () => (
|
||||
Leo Laporte
|
||||
</Link>
|
||||
. © 2007 G4 Media, Inc.
|
||||
</p>
|
||||
</Copyright>
|
||||
</Content>
|
||||
|
||||
<style jsx>{`
|
||||
.copyright {
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.8;
|
||||
margin: 1.25em 1em 0 1em;
|
||||
color: var(--medium-light);
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import Head from "next/head";
|
||||
import { NextSeo } from "next-seo";
|
||||
import Content from "../components/Content/Content";
|
||||
import PageTitle from "../components/PageTitle/PageTitle";
|
||||
@ -23,11 +24,69 @@ import img_2012_09 from "../public/static/images/previously/2012_09.png";
|
||||
import img_2018_04 from "../public/static/images/previously/2018_04.png";
|
||||
import img_2020_03 from "../public/static/images/previously/2020_03.png";
|
||||
|
||||
import "@fontsource/comic-neue/latin-400.css";
|
||||
import "@fontsource/comic-neue/latin-700.css";
|
||||
|
||||
const Previously = () => (
|
||||
<>
|
||||
<Head>
|
||||
{/* a complete sh*tshow of overrides, mainly to compensate for font change */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
body {
|
||||
font-family: "Comic Neue", "Comic Sans MS", "Comic Sans", sans-serif !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
/* left header */
|
||||
header nav > a:first-of-type span:last-of-type {
|
||||
font-size: 1.4em !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
/* right header */
|
||||
header nav ul a span {
|
||||
font-size: 1.1em !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.1;
|
||||
}
|
||||
/* content */
|
||||
main > div > div {
|
||||
font-size: 1.1em !important;
|
||||
text-align: center;
|
||||
}
|
||||
main > div > div p {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
main > div > div strong {
|
||||
font-weight: 900;
|
||||
}
|
||||
main > div > div code {
|
||||
font-size: 0.85em;
|
||||
font-weight: 400;
|
||||
}
|
||||
main > div > div figure:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* footer */
|
||||
footer > div {
|
||||
font-size: 0.95em !important;
|
||||
}
|
||||
/* components */
|
||||
figcaption,
|
||||
.iframe_caption {
|
||||
margin-top: 0.2em;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
color: var(--colors-medium);
|
||||
text-align: center;
|
||||
}
|
||||
hr {
|
||||
margin: 1em auto !important;
|
||||
}
|
||||
iframe {
|
||||
margin-bottom: 0.6em !important;
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<NextSeo
|
||||
title="Previously on..."
|
||||
description="An incredibly embarrassing and somewhat painful trip down this site's memory lane..."
|
||||
@ -152,62 +211,6 @@ const Previously = () => (
|
||||
)
|
||||
</Figure>
|
||||
</Content>
|
||||
|
||||
{/* a complete sh*tshow of overrides, mainly to compensate for font change */}
|
||||
<style jsx global>{`
|
||||
body {
|
||||
font-family: "Comic Neue", "Comic Sans MS", "Comic Sans", var(--font-family-sans-variable);
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
/* left header */
|
||||
header nav > a:first-of-type span:last-of-type {
|
||||
font-size: 1.4em !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
/* right header */
|
||||
header nav ul a span {
|
||||
font-size: 1.1em !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.1;
|
||||
}
|
||||
/* content */
|
||||
main > div > div {
|
||||
font-size: 1.1em !important;
|
||||
text-align: center;
|
||||
}
|
||||
main > div > div p {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
main > div > div strong {
|
||||
font-weight: 900;
|
||||
}
|
||||
main > div > div code {
|
||||
font-size: 0.85em;
|
||||
font-weight: 400;
|
||||
}
|
||||
main > div > div figure:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* footer */
|
||||
footer > div {
|
||||
font-size: 0.95em !important;
|
||||
}
|
||||
/* components */
|
||||
figcaption,
|
||||
.iframe_caption {
|
||||
margin-top: 0.2em;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
color: var(--medium);
|
||||
text-align: center;
|
||||
}
|
||||
hr {
|
||||
margin: 1em auto !important;
|
||||
}
|
||||
iframe {
|
||||
margin-bottom: 0.6em !important;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -5,10 +5,42 @@ import PageTitle from "../components/PageTitle/PageTitle";
|
||||
import Link from "../components/Link/Link";
|
||||
import RepositoryCard from "../components/RepositoryCard/RepositoryCard";
|
||||
import { OctocatOcticon } from "../components/Icons";
|
||||
import { styled } from "../lib/styles/stitches.config";
|
||||
import { authorSocial } from "../lib/config";
|
||||
import type { GetStaticProps } from "next";
|
||||
import type { RepositoryType } from "../types";
|
||||
|
||||
const Wrapper = styled("div", {
|
||||
display: "flex",
|
||||
flexFlow: "row wrap",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
width: "100%",
|
||||
fontSize: "1.1em",
|
||||
lineHeight: 1.1,
|
||||
});
|
||||
|
||||
const Card = styled(RepositoryCard, {
|
||||
flexGrow: 1,
|
||||
margin: "0.6em",
|
||||
width: "370px",
|
||||
});
|
||||
|
||||
const ViewMore = styled("p", {
|
||||
textAlign: "center",
|
||||
marginBottom: 0,
|
||||
fontSize: "1.1em",
|
||||
fontWeight: 500,
|
||||
});
|
||||
|
||||
const GitHubLogo = styled(OctocatOcticon, {
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
verticalAlign: "-0.2em",
|
||||
margin: "0 0.15em",
|
||||
fill: "$text",
|
||||
});
|
||||
|
||||
const Projects = ({ repos }) => (
|
||||
<>
|
||||
<NextSeo
|
||||
@ -21,56 +53,18 @@ const Projects = ({ repos }) => (
|
||||
<PageTitle>💾 Projects</PageTitle>
|
||||
|
||||
<Content>
|
||||
<div className="wrapper">
|
||||
<Wrapper>
|
||||
{repos.map((repo: RepositoryType) => (
|
||||
<div key={repo.name} className="card">
|
||||
<RepositoryCard {...repo} />
|
||||
</div>
|
||||
<Card key={repo.name} {...repo} />
|
||||
))}
|
||||
</div>
|
||||
</Wrapper>
|
||||
|
||||
<p className="view_more">
|
||||
<ViewMore>
|
||||
<Link href={`https://github.com/${authorSocial.github}`}>
|
||||
View more on{" "}
|
||||
<OctocatOcticon
|
||||
fill="currentColor"
|
||||
style={{
|
||||
color: "var(--text)",
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
verticalAlign: "-0.2em",
|
||||
margin: "0 0.15em",
|
||||
}}
|
||||
/>{" "}
|
||||
GitHub...
|
||||
View more on <GitHubLogo /> GitHub...
|
||||
</Link>
|
||||
</p>
|
||||
</ViewMore>
|
||||
</Content>
|
||||
|
||||
<style jsx>{`
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
margin: 0.5em;
|
||||
width: 370px;
|
||||
}
|
||||
|
||||
.view_more {
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
font-size: 1.1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -54,7 +54,7 @@ Y2K.getLayout = (page: ReactElement) => {
|
||||
<Wallpaper
|
||||
image={randomTile}
|
||||
tile
|
||||
style={{
|
||||
css={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
|
@ -29,9 +29,9 @@
|
||||
|
||||
# TECHNOLOGY
|
||||
|
||||
- Next.js
|
||||
- React
|
||||
- Next.js & React
|
||||
- Vercel
|
||||
- Stitches
|
||||
- Giscus
|
||||
- Fathom Analytics
|
||||
- ...and more: https://jarv.is/uses/
|
||||
|
@ -1,19 +0,0 @@
|
||||
body {
|
||||
background-color: var(--background-inner);
|
||||
|
||||
/* light-dark theme switch fading */
|
||||
transition: background 0.25s ease;
|
||||
}
|
||||
|
||||
/* https://web.dev/prefers-reduced-motion/#(bonus)-forcing-reduced-motion-on-all-websites */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
animation-delay: -1ms !important;
|
||||
animation-duration: 1ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
:root {
|
||||
--rounded-edge-radius: 0.65em;
|
||||
--link-underline-size: calc(0.1em + 0.05rem);
|
||||
|
||||
--font-family-sans: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
|
||||
sans-serif;
|
||||
--font-family-sans-variable: InterVariable, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif;
|
||||
--font-family-mono: "Roboto Mono", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier;
|
||||
--font-family-mono-variable: "Roboto MonoVariable", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
|
||||
Courier;
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
body {
|
||||
font-family: var(--font-family-sans);
|
||||
font-kerning: normal;
|
||||
font-variant-ligatures: common-ligatures;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre,
|
||||
.monospace {
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
/*
|
||||
* override above font-family if browser supports variable fonts:
|
||||
* https://caniuse.com/#feat=variable-fonts
|
||||
*/
|
||||
@supports (font-variation-settings: normal) {
|
||||
body {
|
||||
font-family: var(--font-family-sans-variable);
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre,
|
||||
.monospace {
|
||||
font-family: var(--font-family-mono-variable);
|
||||
}
|
||||
|
||||
/*
|
||||
* Chrome doesn't automatically slant multi-axis Inter var, for some reason.
|
||||
* Adding "slnt" -10 fixes Chrome but then over-italicizes in Firefox. AHHHHHHHHHH.
|
||||
*/
|
||||
em {
|
||||
font-style: normal;
|
||||
font-variation-settings: "ital" 1, "slnt" -10;
|
||||
}
|
||||
|
||||
/* Roboto Mono doesn't have this problem, but the above fix breaks it, of course. */
|
||||
em code,
|
||||
em kbd,
|
||||
em samp,
|
||||
em pre,
|
||||
em .monospace {
|
||||
font-style: italic !important;
|
||||
font-variation-settings: initial !important;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user