Page Not Found
+Page Not Found
- - Go home? - ++ Go home? +
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index e298a1ba..bc720ea2 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -16,15 +16,18 @@
"git.fetchOnPull": true,
"git.rebaseWhenSync": true,
"telemetry.telemetryLevel": "off",
+ "javascript.updateImportsOnFileMove.enabled": "always",
"typescript.preferences.importModuleSpecifierEnding": "minimal",
- "typescript.tsdk": "node_modules/typescript/lib"
+ "typescript.surveys.enabled": false,
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "typescript.tsserver.log": "off",
+ "typescript.updateImportsOnFileMove.enabled": "always"
},
"extensions": [
+ "bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
- "EditorConfig.EditorConfig",
- "unifiedjs.vscode-mdx",
"esbenp.prettier-vscode",
- "stylelint.vscode-stylelint"
+ "unifiedjs.vscode-mdx"
]
}
},
diff --git a/.npmrc b/.npmrc
index 6f07069f..d93a75a6 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,3 +1,2 @@
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
-public-hoist-pattern[]=*stylelint*
diff --git a/.prettierrc.mjs b/.prettierrc.mjs
index 49b1fa75..1be2c63a 100644
--- a/.prettierrc.mjs
+++ b/.prettierrc.mjs
@@ -1,12 +1,13 @@
/** @type {import("prettier").Config} */
const config = {
- singleQuote: false,
+ plugins: ["prettier-plugin-tailwindcss"],
jsxSingleQuote: false,
printWidth: 120,
- tabWidth: 2,
- useTabs: false,
quoteProps: "as-needed",
+ singleQuote: false,
+ tabWidth: 2,
trailingComma: "es5",
+ useTabs: false,
};
export default config;
diff --git a/.stylelintrc.mjs b/.stylelintrc.mjs
deleted file mode 100644
index dd849dc0..00000000
--- a/.stylelintrc.mjs
+++ /dev/null
@@ -1,19 +0,0 @@
-/* eslint-disable import/no-anonymous-default-export */
-
-/** @type {import("stylelint").Config} */
-export default {
- extends: ["stylelint-config-standard", "stylelint-config-css-modules", "stylelint-prettier/recommended"],
- rules: {
- "selector-class-pattern": null,
- "custom-property-pattern": null,
- "media-feature-range-notation": null,
- "rule-empty-line-before": [
- "always-multi-line",
- {
- except: ["after-single-line-comment"],
- ignore: ["inside-block"],
- },
- ],
- "color-hex-length": "long",
- },
-};
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..2229ccd0
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,8 @@
+{
+ "recommendations": [
+ "bradlc.vscode-tailwindcss",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "unifiedjs.vscode-mdx"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..fe47dfc4
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,16 @@
+{
+ "editor.tabSize": 2,
+ "editor.rulers": [
+ 120
+ ],
+ "files.associations": {
+ "*.css": "tailwindcss",
+ "*.mdx": "markdown"
+ },
+ "javascript.updateImportsOnFileMove.enabled": "always",
+ "typescript.preferences.importModuleSpecifierEnding": "minimal",
+ "typescript.surveys.enabled": false,
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "typescript.tsserver.log": "off",
+ "typescript.updateImportsOnFileMove.enabled": "always"
+}
diff --git a/app/contact/form.module.css b/app/contact/form.module.css
index 743e9db3..0821bae8 100644
--- a/app/contact/form.module.css
+++ b/app/contact/form.module.css
@@ -2,15 +2,15 @@
width: 100%;
padding: 0.8em;
margin: 0.6em 0;
- border: 2px solid var(--colors-light);
+ border: 2px solid var(--color-gray-400);
border-radius: 0.6em;
color: var(--colors-text);
- background-color: var(--colors-super-duper-light);
+ background-color: var(--color-gray-100);
}
.input:focus {
outline: none;
- border-color: var(--colors-link);
+ border-color: var(--color-link);
}
.input.textarea {
@@ -21,12 +21,12 @@
}
.input.invalid {
- border-color: var(--colors-error);
+ border-color: var(--color-error);
}
.errorMessage {
font-size: 0.9em;
- color: var(--colors-error);
+ color: var(--color-error);
}
.actionRow {
@@ -48,13 +48,13 @@
user-select: none;
font-weight: 500;
color: var(--colors-text);
- background-color: var(--colors-kinda-light);
+ background-color: var(--color-gray-300);
}
.submitButton:hover,
.submitButton:focus-visible {
- color: var(--colors-super-duper-light);
- background-color: var(--colors-link);
+ color: var(--color-gray-100);
+ background-color: var(--color-link);
}
.submitIcon {
@@ -70,11 +70,11 @@
}
.result.success {
- color: var(--colors-success);
+ color: var(--color-success);
}
.result.error {
- color: var(--colors-error);
+ color: var(--color-error);
}
.resultIcon {
diff --git a/app/contact/form.tsx b/app/contact/form.tsx
index 28ed5910..7cab7590 100644
--- a/app/contact/form.tsx
+++ b/app/contact/form.tsx
@@ -97,7 +97,7 @@ const ContactForm = () => {
Markdown syntax
{" "}
is allowed here, e.g.: **bold**, _italics_, [
-
+
links
](https://jarv.is), and `code`
.
diff --git a/app/contact/page.tsx b/app/contact/page.tsx
index 1e03f0fd..e6803490 100644
--- a/app/contact/page.tsx
+++ b/app/contact/page.tsx
@@ -32,7 +32,7 @@ const Page = () => {
size="0.975em"
style={{
marginRight: "0.15em",
- stroke: "var(--colors-warning)",
+ stroke: "var(--color-warning)",
verticalAlign: "middle",
}}
/>{" "}
diff --git a/app/fonts.ts b/app/fonts.ts
index 548cac31..2ef1555a 100644
--- a/app/fonts.ts
+++ b/app/fonts.ts
@@ -11,7 +11,7 @@ export const GeistSans = GeistSansLoader({
"system-ui",
"sans-serif",
],
- variable: "--fonts-sans",
+ variable: "--font-geist-sans",
preload: true,
});
@@ -29,6 +29,6 @@ export const GeistMono = GeistMonoLoader({
"monospace",
],
adjustFontFallback: false,
- variable: "--fonts-mono",
+ variable: "--font-geist-mono",
preload: true,
});
diff --git a/app/globals.css b/app/globals.css
index b1046191..b3429bb9 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,69 +1,98 @@
-/*!
- * modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize/tree/v3.0.1
- */
+@import "tailwindcss";
-*,
-::before,
-::after {
- box-sizing: border-box;
+@theme inline {
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+
+ --container-default: var(--container-4xl);
}
-html {
- line-height: 1.15;
- tab-size: 4;
- /* stylelint-disable-next-line property-no-vendor-prefix */
- -webkit-text-size-adjust: 100%;
+@theme {
+ --color-*: initial;
+ --color-background-inner: oklch(1 0 0);
+ --color-background-outer: oklch(0.99 0 0);
+ --color-gray-900: oklch(0.23 0 0);
+ --color-gray-800: oklch(0.32 0 0);
+ --color-gray-700: oklch(0.43 0 0);
+ --color-gray-600: oklch(0.48 0 0);
+ --color-gray-500: oklch(0.56 0 0);
+ --color-gray-400: oklch(0.66 0 0);
+ --color-gray-300: oklch(0.78 0 0);
+ --color-gray-200: oklch(0.85 0 0);
+ --color-gray-100: oklch(0.99 0 0);
+ --color-link: oklch(0.53 0.1547 252.33);
+ --color-success: oklch(0.63 0.1557 144.2);
+ --color-warning: oklch(0.72 0.177693 55.7508);
+ --color-error: oklch(0.64 0.2505 28.39);
+
+ --animate-wave: wave 5s ease 1s infinite;
+ --animate-heartbeat: heartbeat 10s ease 7.5s infinite;
+
+ @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);
+ }
+ 100% {
+ transform: rotate(0deg);
+ }
+ }
+
+ @keyframes heartbeat {
+ 0% {
+ transform: scale(1);
+ }
+ 2% {
+ transform: scale(1.25);
+ }
+ 4% {
+ transform: scale(1);
+ }
+ 6% {
+ transform: scale(1.2);
+ }
+ 8% {
+ transform: scale(1);
+ }
+ 100% {
+ transform: scale(1);
+ }
+ }
}
-body {
- margin: 0;
- font-family: var(--fonts-sans);
- background-color: var(--colors-background-inner);
-}
+@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
-code,
-kbd,
-samp,
-pre {
- font-size: 1em;
- font-family: var(--fonts-mono);
- font-variant-ligatures: none; /* i hate them. fwiw. */
-}
-
-small {
- font-size: 80%;
-}
-
-sub,
-sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-sup {
- top: -0.5em;
-}
-
-button,
-input,
-select,
-textarea {
- font-family: inherit;
- font-size: 100%;
- line-height: 1.15;
- margin: 0;
-}
-
-button,
-[type="button"],
-[type="reset"],
-[type="submit"] {
- /* stylelint-disable-next-line property-no-vendor-prefix */
- -webkit-appearance: button;
+&:where([data-theme=dark], [data-theme=dark] *) {
+ --color-background-inner: oklch(0.24 0 0);
+ --color-background-outer: oklch(0.26 0 0);
+ --color-gray-900: oklch(0.93 0 0);
+ --color-gray-800: oklch(0.90 0 0);
+ --color-gray-700: oklch(0.88 0 0);
+ --color-gray-600: oklch(0.76 0 0);
+ --color-gray-500: oklch(0.67 0 0);
+ --color-gray-400: oklch(0.5 0 0);
+ --color-gray-300: oklch(0.44 0 0);
+ --color-gray-200: oklch(0.35 0 0);
+ --color-gray-100: oklch(0.24 0 0);
+ --color-link: oklch(0.81 0.1026 246.31);
+ --color-success: oklch(0.81 0.2001 138.65);
+ --color-warning: oklch(0.81 0.166 85.03);
+ --color-error: oklch(0.68 0.2105 24.73)
}
diff --git a/app/hillary/page.tsx b/app/hillary/page.tsx
index 8c20e236..4c4b41a1 100644
--- a/app/hillary/page.tsx
+++ b/app/hillary/page.tsx
@@ -57,7 +57,7 @@ const Page = () => {
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
- color: "var(--colors-medium-light)",
+ color: "var(--color-gray-500)",
}}
>
Video is property of{" "}
diff --git a/app/layout.module.css b/app/layout.module.css
deleted file mode 100644
index acb47ca0..00000000
--- a/app/layout.module.css
+++ /dev/null
@@ -1,26 +0,0 @@
-.layout {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
-}
-
-.default {
- width: 100%;
- padding: 1.5em;
- font-size: 0.9em;
- line-height: 1.7;
- color: var(--colors-text);
-}
-
-.container {
- width: 100%;
- max-width: var(--max-width);
- margin: 0 auto;
-}
-
-@media (max-width: 768px) {
- .default {
- font-size: 0.925em;
- line-height: 1.85;
- }
-}
diff --git a/app/layout.tsx b/app/layout.tsx
index 37bd982c..ca614d49 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,27 +1,27 @@
import { env } from "../lib/env";
import { JsonLd } from "react-schemaorg";
import { Analytics } from "@vercel/analytics/next";
-import clsx from "clsx";
import { ThemeProvider, ThemeScript } from "../contexts/ThemeContext";
import Header from "../components/Header";
import Footer from "../components/Footer";
import { SkipNavLink, SkipNavTarget } from "../components/SkipNav";
+import cn from "../lib/helpers/classnames";
import { defaultMetadata } from "../lib/helpers/metadata";
import * as config from "../lib/config";
-import { MAX_WIDTH } from "../lib/config/constants";
import type { Person, WebSite } from "schema-dts";
import { GeistMono, GeistSans } from "./fonts";
import "./globals.css";
-import "./themes.css";
-
-import styles from "./layout.module.css";
export const metadata = defaultMetadata;
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
-
+
+ Go home? +
- Hi there! I’m Jake. 👋 + <> +
+ Hi there! I’m Jake. 👋
-+
I’m a frontend web developer based in the{" "} Boston {" "} area.
-+
I specialize in using{" "} - + TypeScript ,{" "} - + React , and{" "} - + Next.js {" "} to make lightweight{" "} Jamstack sites {" "} with dynamic and powerful{" "} - + Node {" "} backends. But I still know my way around{" "} less buzzwordy {" "} @@ -107,21 +56,19 @@ const Page = () => { vanilla JavaScript ), too.
-+
Whenever possible, I also apply my experience in{" "} application security @@ -129,8 +76,7 @@ const Page = () => { serverless stacks @@ -138,21 +84,19 @@ const Page = () => { DevOps automation .
-+
I fell in love with{" "} frontend web design {" "} @@ -160,8 +104,7 @@ const Page = () => { backend programming {" "} @@ -169,86 +112,76 @@ const Page = () => {🪄 ") 5 5, auto`,
}}
>
the Tooth Fairy
- . I’ve improved a bit since then, I think? 🤷
+ . I’ve improved a bit since then, I think? 🤷
-+
Over the years, some of my side projects{" "} have {" "} been {" "} featured {" "} by {" "} various {" "} media {" "} outlets .
-+
You can find my work on{" "} GitHub {" "} @@ -256,44 +189,30 @@ const Page = () => { LinkedIn . I’m always available to connect over{" "} - + email {" "} - + - {" "}
- {" "}
+
-- 2B0C 9CF2 51E6 9A39 -
+2B0C 9CF2 51E6 9A39
,{" "} Bluesky @@ -301,15 +220,13 @@ const Page = () => { Mastodon {" "} as well!- +Failed to generate activity calendar.}>
-
+
+
+
-
+ Contribution activity
+
+
{repos?.map((repo) => (
-
-
+
+
{repo!.name}
- {repo!.description &&
+
{repo!.primaryLanguage && (
-
+
{repo!.primaryLanguage.color && (
)}
@@ -75,41 +85,35 @@ const Page = async () => {
)}
{repo!.stargazerCount > 0 && (
-
+
-
+
{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)}
)}
{repo!.forkCount > 0 && (
-
+
-
+
{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)}
)}
-
+
Updated
@@ -120,27 +124,15 @@ const Page = async () => {
))}
-
Popular repositories
-{repo!.description}
} + {repo!.description &&{repo!.description}
} -- +
+ View more on{" "} {" "} diff --git a/app/themes.css b/app/themes.css deleted file mode 100644 index 57805c48..00000000 --- a/app/themes.css +++ /dev/null @@ -1,37 +0,0 @@ -:root { - --colors-background-inner: #ffffff; - --colors-background-outer: #fcfcfc; - --colors-background-header: rgb(252 252 252 / 70%); - --colors-text: #202020; - --colors-medium-dark: #515151; - --colors-medium: #5e5e5e; - --colors-medium-light: #757575; - --colors-light: #d2d2d2; - --colors-kinda-light: #e3e3e3; - --colors-super-light: #f4f4f4; - --colors-super-duper-light: #fbfbfb; - --colors-link: #0e6dc2; - --colors-link-underline: #a6c5e7; - --colors-success: #44a248; - --colors-error: #ff1b1b; - --colors-warning: #f78200; -} - -[data-theme="dark"] { - --colors-background-inner: #1e1e1e; - --colors-background-outer: #252525; - --colors-background-header: rgb(37 37 37 / 70%); - --colors-text: #f1f1f1; - --colors-medium-dark: #d7d7d7; - --colors-medium: #b1b1b1; - --colors-medium-light: #959595; - --colors-light: #646464; - --colors-kinda-light: #535353; - --colors-super-light: #272727; - --colors-super-duper-light: #1f1f1f; - --colors-link: #88c7ff; - --colors-link-underline: #496278; - --colors-success: #78df55; - --colors-error: #ff5151; - --colors-warning: #f2b702; -} diff --git a/app/zip/page.tsx b/app/zip/page.tsx index 65a3515a..6fb45598 100644 --- a/app/zip/page.tsx +++ b/app/zip/page.tsx @@ -32,20 +32,20 @@ const Page = () => { >
sundar@google: ~$ mv /root - + /stable_products_that_people_rely_on/ googledomains.zip /tmp/ @@ -64,7 +64,7 @@ const Page = () => {
+
) : (
)}
diff --git a/components/Footer/Footer.module.css b/components/Footer/Footer.module.css
deleted file mode 100644
index 34cfac04..00000000
--- a/components/Footer/Footer.module.css
+++ /dev/null
@@ -1,88 +0,0 @@
-.footer {
- flex: 1;
- width: 100%;
- padding: 1.25em 1.5em;
- border-top: 1px solid var(--colors-kinda-light);
- background-color: var(--colors-background-outer);
- color: var(--colors-medium-dark);
-}
-
-.row {
- display: flex;
- justify-content: space-between;
- width: 100%;
- max-width: var(--max-width);
- margin: 0 auto;
- font-size: 0.8em;
- line-height: 2.3;
-}
-
-.link {
- color: var(--colors-medium-dark) !important;
-}
-
-.link:has(.icon):hover,
-.link:has(.icon):focus-visible {
- color: var(--colors-medium) !important;
-}
-
-.link.underline {
- padding-bottom: 2px;
- border-bottom: 1px solid var(--colors-light);
-}
-
-.link.underline:hover,
-.link.underline:focus-visible {
- border-bottom-color: var(--colors-kinda-light);
-}
-
-.icon {
- width: 1.25em;
- height: 1.25em;
- margin: 0 0.1em;
- vertical-align: text-top;
-}
-
-.heart {
- color: var(--colors-error);
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .heart {
- animation: pulse 10s ease 7.5s infinite;
- }
-
- @keyframes pulse {
- 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);
- }
- }
-}
-
-@media (max-width: 768px) {
- .footer {
- padding: 1em 1.25em;
- }
-
- /* stack columns on left instead of flexboxing across */
- .row {
- display: block;
- }
-}
diff --git a/components/Footer/Footer.tsx b/components/Footer/Footer.tsx
index c449f34c..738ae487 100644
--- a/components/Footer/Footer.tsx
+++ b/components/Footer/Footer.tsx
@@ -1,38 +1,44 @@
import { env } from "../../lib/env";
-import clsx from "clsx";
import { HeartIcon } from "lucide-react";
import Link from "../Link";
+import cn from "../../lib/helpers/classnames";
import * as config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react";
-import styles from "./Footer.module.css";
-
export type FooterProps = ComponentPropsWithoutRef<"footer">;
const Footer = ({ className, ...rest }: FooterProps) => {
return (
-
@monthly rm -f /tmp/ - + *.zip
diff --git a/components/Blockquote/Blockquote.module.css b/components/Blockquote/Blockquote.module.css deleted file mode 100644 index b89de55f..00000000 --- a/components/Blockquote/Blockquote.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.blockquote { - margin-left: 0; - padding-left: 1.25em; - border-left: 0.25em solid var(--colors-link); - color: var(--colors-medium-dark); -} diff --git a/components/Blockquote/Blockquote.tsx b/components/Blockquote/Blockquote.tsx index 33feec8a..07376347 100644 --- a/components/Blockquote/Blockquote.tsx +++ b/components/Blockquote/Blockquote.tsx @@ -1,12 +1,10 @@ -import clsx from "clsx"; +import cn from "../../lib/helpers/classnames"; import type { ComponentPropsWithoutRef } from "react"; -import styles from "./Blockquote.module.css"; - export type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">; const Blockquote = ({ className, ...rest }: BlockquoteProps) => ( - + ); export default Blockquote; diff --git a/components/Code/Code.module.css b/components/Code/Code.module.css index 5612590c..6b3a3369 100644 --- a/components/Code/Code.module.css +++ b/components/Code/Code.module.css @@ -7,8 +7,8 @@ font-size: 0.925em; tab-size: 2px; page-break-inside: avoid; - background-color: var(--colors-background-header); - border: 1px solid var(--colors-kinda-light); + background-color: var(--color-background-header); + border: 1px solid var(--color-gray-300); border-radius: 0.6em; } @@ -55,7 +55,7 @@ figure .code[data-line-numbers] > [data-line]::before { width: 1em; margin-right: 1.5em; text-align: right; - color: var(--colors-medium-light); + color: var(--color-gray-500); user-select: none; counter-increment: line; content: counter(line); @@ -76,11 +76,11 @@ figure .code[data-line-numbers-max-digits="3"] > [data-line]::before { height: 3em; width: 3em; padding: 0; /* iOS safari fix */ - color: var(--colors-medium-dark); - border: 1px solid var(--colors-kinda-light); + color: var(--color-gray-700); + border: 1px solid var(--color-gray-300); border-top-right-radius: 0.6em; border-bottom-left-radius: 0.6em; - background-color: var(--colors-background-header); + background-color: var(--color-background-header); backdrop-filter: saturate(180%) blur(5px); } @@ -92,5 +92,5 @@ figure .code[data-line-numbers-max-digits="3"] > [data-line]::before { .copyButton:hover, .copyButton:focus-visible { - color: var(--colors-link); + color: var(--color-link); } diff --git a/components/CopyButton/CopyButton.tsx b/components/CopyButton/CopyButton.tsx index cf7fe211..b6260595 100644 --- a/components/CopyButton/CopyButton.tsx +++ b/components/CopyButton/CopyButton.tsx @@ -52,7 +52,7 @@ const CopyButton = ({ source, timeout = 2000, style, ...rest }: CopyButtonProps, {...rest} > {copied ? ( -