mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 09:05:22 -04:00
initial tailwind conversion
This commit is contained in:
parent
f277119407
commit
c9a8b3eaa5
@ -16,15 +16,18 @@
|
|||||||
"git.fetchOnPull": true,
|
"git.fetchOnPull": true,
|
||||||
"git.rebaseWhenSync": true,
|
"git.rebaseWhenSync": true,
|
||||||
"telemetry.telemetryLevel": "off",
|
"telemetry.telemetryLevel": "off",
|
||||||
|
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
"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": [
|
"extensions": [
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"unifiedjs.vscode-mdx",
|
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"stylelint.vscode-stylelint"
|
"unifiedjs.vscode-mdx"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
1
.npmrc
1
.npmrc
@ -1,3 +1,2 @@
|
|||||||
public-hoist-pattern[]=*eslint*
|
public-hoist-pattern[]=*eslint*
|
||||||
public-hoist-pattern[]=*prettier*
|
public-hoist-pattern[]=*prettier*
|
||||||
public-hoist-pattern[]=*stylelint*
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
/** @type {import("prettier").Config} */
|
/** @type {import("prettier").Config} */
|
||||||
const config = {
|
const config = {
|
||||||
singleQuote: false,
|
plugins: ["prettier-plugin-tailwindcss"],
|
||||||
jsxSingleQuote: false,
|
jsxSingleQuote: false,
|
||||||
printWidth: 120,
|
printWidth: 120,
|
||||||
tabWidth: 2,
|
|
||||||
useTabs: false,
|
|
||||||
quoteProps: "as-needed",
|
quoteProps: "as-needed",
|
||||||
|
singleQuote: false,
|
||||||
|
tabWidth: 2,
|
||||||
trailingComma: "es5",
|
trailingComma: "es5",
|
||||||
|
useTabs: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
@ -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",
|
|
||||||
},
|
|
||||||
};
|
|
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"unifiedjs.vscode-mdx"
|
||||||
|
]
|
||||||
|
}
|
16
.vscode/settings.json
vendored
Normal file
16
.vscode/settings.json
vendored
Normal file
@ -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"
|
||||||
|
}
|
@ -97,7 +97,7 @@ const ContactForm = () => {
|
|||||||
Markdown syntax
|
Markdown syntax
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
|
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
|
||||||
<Link href="https://jarv.is" plain>
|
<Link href="https://jarv.is" className="hover:no-underline">
|
||||||
links
|
links
|
||||||
</Link>
|
</Link>
|
||||||
](https://jarv.is), and <code>`code`</code>.
|
](https://jarv.is), and <code>`code`</code>.
|
||||||
|
@ -11,7 +11,7 @@ export const GeistSans = GeistSansLoader({
|
|||||||
"system-ui",
|
"system-ui",
|
||||||
"sans-serif",
|
"sans-serif",
|
||||||
],
|
],
|
||||||
variable: "--fonts-sans",
|
variable: "--font-geist-sans",
|
||||||
preload: true,
|
preload: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -29,6 +29,6 @@ export const GeistMono = GeistMonoLoader({
|
|||||||
"monospace",
|
"monospace",
|
||||||
],
|
],
|
||||||
adjustFontFallback: false,
|
adjustFontFallback: false,
|
||||||
variable: "--fonts-mono",
|
variable: "--font-geist-mono",
|
||||||
preload: true,
|
preload: true,
|
||||||
});
|
});
|
||||||
|
155
app/globals.css
155
app/globals.css
@ -1,69 +1,100 @@
|
|||||||
/*!
|
@import "tailwindcss";
|
||||||
* modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize/tree/v3.0.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
*,
|
@theme inline {
|
||||||
::before,
|
--font-sans: var(--font-geist-sans);
|
||||||
::after {
|
--font-mono: var(--font-geist-mono);
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
@theme {
|
||||||
line-height: 1.15;
|
--container-default: var(--container-4xl);
|
||||||
tab-size: 4;
|
|
||||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
--color-*: initial;
|
||||||
-webkit-text-size-adjust: 100%;
|
--color-background-inner: #ffffff;
|
||||||
|
--color-background-outer: #fcfcfc;
|
||||||
|
--color-text: #202020;
|
||||||
|
--color-medium-dark: #515151;
|
||||||
|
--color-medium: #5e5e5e;
|
||||||
|
--color-medium-light: #757575;
|
||||||
|
--color-light: #d2d2d2;
|
||||||
|
--color-kinda-light: #e3e3e3;
|
||||||
|
--color-super-light: #f4f4f4;
|
||||||
|
--color-super-duper-light: #fbfbfb;
|
||||||
|
--color-link: #0e6dc2;
|
||||||
|
--color-success: #44a248;
|
||||||
|
--color-error: #ff1b1b;
|
||||||
|
--color-warning: #f78200;
|
||||||
|
|
||||||
|
--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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pause for ~9 out of 10 seconds */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pause for ~9 out of 10 seconds */
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||||
margin: 0;
|
|
||||||
font-family: var(--fonts-sans);
|
|
||||||
background-color: var(--colors-background-inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
code,
|
[data-theme="dark"] {
|
||||||
kbd,
|
--color-background-inner: #1e1e1e;
|
||||||
samp,
|
--color-background-outer: #252525;
|
||||||
pre {
|
--color-text: #f1f1f1;
|
||||||
font-size: 1em;
|
--color-medium-dark: #d7d7d7;
|
||||||
font-family: var(--fonts-mono);
|
--color-medium: #b1b1b1;
|
||||||
font-variant-ligatures: none; /* i hate them. fwiw. */
|
--color-medium-light: #959595;
|
||||||
}
|
--color-light: #646464;
|
||||||
|
--color-kinda-light: #535353;
|
||||||
small {
|
--color-super-light: #272727;
|
||||||
font-size: 80%;
|
--color-super-duper-light: #1f1f1f;
|
||||||
}
|
--color-link: #88c7ff;
|
||||||
|
--color-success: #78df55;
|
||||||
sub,
|
--color-error: #ff5151;
|
||||||
sup {
|
--color-warning: #f2b702;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +1,27 @@
|
|||||||
import { env } from "../lib/env";
|
import { env } from "../lib/env";
|
||||||
import { JsonLd } from "react-schemaorg";
|
import { JsonLd } from "react-schemaorg";
|
||||||
import { Analytics } from "@vercel/analytics/next";
|
import { Analytics } from "@vercel/analytics/next";
|
||||||
import clsx from "clsx";
|
|
||||||
import { ThemeProvider, ThemeScript } from "../contexts/ThemeContext";
|
import { ThemeProvider, ThemeScript } from "../contexts/ThemeContext";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
import Footer from "../components/Footer";
|
import Footer from "../components/Footer";
|
||||||
import { SkipNavLink, SkipNavTarget } from "../components/SkipNav";
|
import { SkipNavLink, SkipNavTarget } from "../components/SkipNav";
|
||||||
|
import cn from "../lib/helpers/classnames";
|
||||||
import { defaultMetadata } from "../lib/helpers/metadata";
|
import { defaultMetadata } from "../lib/helpers/metadata";
|
||||||
import * as config from "../lib/config";
|
import * as config from "../lib/config";
|
||||||
import { MAX_WIDTH } from "../lib/config/constants";
|
|
||||||
import type { Person, WebSite } from "schema-dts";
|
import type { Person, WebSite } from "schema-dts";
|
||||||
|
|
||||||
import { GeistMono, GeistSans } from "./fonts";
|
import { GeistMono, GeistSans } from "./fonts";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import "./themes.css";
|
|
||||||
|
|
||||||
import styles from "./layout.module.css";
|
|
||||||
|
|
||||||
export const metadata = defaultMetadata;
|
export const metadata = defaultMetadata;
|
||||||
|
|
||||||
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||||
return (
|
return (
|
||||||
<html lang={env.NEXT_PUBLIC_SITE_LOCALE} suppressHydrationWarning>
|
<html
|
||||||
|
lang={env.NEXT_PUBLIC_SITE_LOCALE}
|
||||||
|
className={cn(GeistSans.variable, GeistMono.variable)}
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
<head>
|
<head>
|
||||||
<ThemeScript />
|
<ThemeScript />
|
||||||
|
|
||||||
@ -62,21 +62,16 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body
|
<body className="bg-background-outer text-text font-sans">
|
||||||
className={clsx(GeistSans.variable, GeistMono.variable)}
|
|
||||||
style={{ ["--max-width" as string]: `${MAX_WIDTH}px` }}
|
|
||||||
>
|
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SkipNavLink />
|
<SkipNavLink />
|
||||||
|
|
||||||
<div className={styles.layout}>
|
<div className="mx-auto flex min-h-screen flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main className={styles.default}>
|
<main className="bg-background-inner w-full text-sm">
|
||||||
<div className={styles.container}>
|
<SkipNavTarget />
|
||||||
<SkipNavTarget />
|
<div className="max-w-default mx-auto p-5">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
@ -88,7 +88,10 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
<Link href={`/${POSTS_DIR}/${frontmatter!.slug}`} plain className={clsx(styles.metaItem, styles.metaLink)}>
|
<Link
|
||||||
|
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
|
||||||
|
className={clsx("hover:no-underline", styles.metaItem, styles.metaLink)}
|
||||||
|
>
|
||||||
<CalendarDaysIcon size="1.25em" className={styles.metaIcon} />
|
<CalendarDaysIcon size="1.25em" className={styles.metaIcon} />
|
||||||
<Time date={frontmatter!.date} format="MMMM d, y" />
|
<Time date={frontmatter!.date} format="MMMM d, y" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -109,8 +112,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
|||||||
<Link
|
<Link
|
||||||
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter!.slug}/index.mdx`}
|
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter!.slug}/index.mdx`}
|
||||||
title={`Edit "${frontmatter!.title}" on GitHub`}
|
title={`Edit "${frontmatter!.title}" on GitHub`}
|
||||||
plain
|
className={clsx("hover:no-underline", styles.metaItem, styles.metaLink)}
|
||||||
className={clsx(styles.metaItem, styles.metaLink)}
|
|
||||||
>
|
>
|
||||||
<SquarePenIcon size="1.25em" className={styles.metaIcon} />
|
<SquarePenIcon size="1.25em" className={styles.metaIcon} />
|
||||||
<span>Improve This Post</span>
|
<span>Improve This Post</span>
|
||||||
@ -139,8 +141,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
|||||||
<Link
|
<Link
|
||||||
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
|
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
|
||||||
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
|
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
|
||||||
plain
|
className={clsx("hover:no-underline", styles.link)}
|
||||||
className={styles.link}
|
|
||||||
/>
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
.index h1 {
|
|
||||||
margin: 0 0 0.5em -1px; /* misaligned left margin, super nitpicky */
|
|
||||||
font-size: 1.925em;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index h2 {
|
|
||||||
margin: 0.5em 0 0.5em -1px;
|
|
||||||
font-size: 1.3em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index p {
|
|
||||||
margin: 0.85em 0;
|
|
||||||
font-size: 1.05em;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index p:last-of-type {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index sup {
|
|
||||||
margin: 0 0.1em;
|
|
||||||
font-size: 0.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wave {
|
|
||||||
margin-left: 0.1em;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.wave {
|
|
||||||
animation: wave 5s ease 1s infinite;
|
|
||||||
transform-origin: 65% 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 ~9 out of 10 seconds */
|
|
||||||
100% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.index h1 {
|
|
||||||
font-size: 1.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index h2 {
|
|
||||||
font-size: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.index p {
|
|
||||||
font-size: 1em;
|
|
||||||
line-height: 1.9;
|
|
||||||
}
|
|
||||||
}
|
|
169
app/page.tsx
169
app/page.tsx
@ -1,105 +1,58 @@
|
|||||||
import clsx from "clsx";
|
import Link from "../components/Link";
|
||||||
import hash from "@emotion/hash";
|
import cn from "../lib/helpers/classnames";
|
||||||
import { rgba } from "polished";
|
|
||||||
import { LockIcon } from "lucide-react";
|
import { LockIcon } from "lucide-react";
|
||||||
import UnstyledLink from "../components/Link";
|
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
|
||||||
|
|
||||||
import styles from "./page.module.css";
|
|
||||||
|
|
||||||
const Link = ({
|
|
||||||
lightColor,
|
|
||||||
darkColor,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...rest
|
|
||||||
}: ComponentPropsWithoutRef<typeof UnstyledLink> & {
|
|
||||||
lightColor?: string;
|
|
||||||
darkColor?: string;
|
|
||||||
}) => {
|
|
||||||
if (lightColor && darkColor) {
|
|
||||||
const uniqueId = hash(`${lightColor},${darkColor}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UnstyledLink className={clsx(`t_${uniqueId}`, className)} {...rest}>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<style
|
|
||||||
// workaround to have react combine all of these inline styles into a single <style> tag up top, see:
|
|
||||||
// https://react.dev/reference/react-dom/components/style#rendering-an-inline-css-stylesheet
|
|
||||||
href={uniqueId}
|
|
||||||
precedence={styles.index}
|
|
||||||
>
|
|
||||||
{`.t_${uniqueId}{--colors-link:${lightColor};--colors-link-underline:${rgba(lightColor, 0.4)}}[data-theme="dark"] .t_${uniqueId}{--colors-link:${darkColor};--colors-link-underline:${rgba(darkColor, 0.4)}}`}
|
|
||||||
</style>
|
|
||||||
</UnstyledLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UnstyledLink className={className} {...rest}>
|
|
||||||
{children}
|
|
||||||
</UnstyledLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.index}>
|
<>
|
||||||
<h1>
|
<h1 className="mb-2 text-3xl leading-5 font-medium">
|
||||||
Hi there! I’m Jake. <span className={styles.wave}>👋</span>
|
Hi there! I’m Jake. <span className="animate-wave ml-0.5 inline-block origin-[65%_80%] text-3xl">👋</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h2>
|
<h2 className="my-4 text-xl leading-6 font-normal">
|
||||||
I’m a frontend web developer based in the{" "}
|
I’m a frontend web developer based in the{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&iv_load_policy=3"
|
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'
|
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
|
||||||
lightColor="#fb4d42"
|
className={cn(`[--color-link:#fb4d42]`, `dark:[--color-link:#ff5146]`)}
|
||||||
darkColor="#ff5146"
|
|
||||||
>
|
>
|
||||||
Boston
|
Boston
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
area.
|
area.
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>
|
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
|
||||||
I specialize in using{" "}
|
I specialize in using{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://www.typescriptlang.org/"
|
href="https://www.typescriptlang.org/"
|
||||||
title="TypeScript Official Website"
|
className={cn(`[--color-link:#235a97]`, `dark:[--color-link:#59a8ff]`)}
|
||||||
lightColor="#235a97"
|
|
||||||
darkColor="#59a8ff"
|
|
||||||
>
|
>
|
||||||
TypeScript
|
TypeScript
|
||||||
</Link>
|
</Link>
|
||||||
,{" "}
|
,{" "}
|
||||||
<Link href="https://reactjs.org/" title="React Official Website" lightColor="#1091b3" darkColor="#6fcbe3">
|
<Link href="https://reactjs.org/" className={cn(`[--color-link:#1091b3]`, `dark:[--color-link:#6fcbe3]`)}>
|
||||||
React
|
React
|
||||||
</Link>
|
</Link>
|
||||||
, and{" "}
|
, and{" "}
|
||||||
<Link href="https://nextjs.org/" title="Next.js Official Website" lightColor="#5e7693" darkColor="#a8b9c0">
|
<Link href="https://nextjs.org/" className={cn(`[--color-link:#5e7693]`, `dark:[--color-link:#a8b9c0]`)}>
|
||||||
Next.js
|
Next.js
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
to make lightweight{" "}
|
to make lightweight{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://jamstack.org/glossary/jamstack/"
|
href="https://jamstack.org/glossary/jamstack/"
|
||||||
title="Jamstack Glossary"
|
className={cn(`[--color-link:#04a699]`, `dark:[--color-link:#08bbac]`)}
|
||||||
lightColor="#04a699"
|
|
||||||
darkColor="#08bbac"
|
|
||||||
>
|
>
|
||||||
Jamstack sites
|
Jamstack sites
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
with dynamic and powerful{" "}
|
with dynamic and powerful{" "}
|
||||||
<Link href="https://nodejs.org/en/" title="Node.js Official Website" lightColor="#6fbc4e" darkColor="#84d95f">
|
<Link href="https://nodejs.org/en/" className={cn(`[--color-link:#6fbc4e]`, `dark:[--color-link:#84d95f]`)}>
|
||||||
Node
|
Node
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
backends. But I still know my way around{" "}
|
backends. But I still know my way around{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://www.jetbrains.com/lp/php-25/"
|
href="https://www.jetbrains.com/lp/php-25/"
|
||||||
title="25 Years of PHP History"
|
title="25 Years of PHP History"
|
||||||
lightColor="#8892bf"
|
className={cn(`[--color-link:#8892bf]`, `dark:[--color-link:#a4afe3]`)}
|
||||||
darkColor="#a4afe3"
|
|
||||||
>
|
>
|
||||||
less buzzwordy
|
less buzzwordy
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
@ -107,21 +60,19 @@ const Page = () => {
|
|||||||
<Link
|
<Link
|
||||||
href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/"
|
href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/"
|
||||||
title='"The Cost of Javascript Frameworks" by Tim Kadlec'
|
title='"The Cost of Javascript Frameworks" by Tim Kadlec'
|
||||||
lightColor="#f48024"
|
className={cn(`[--color-link:#f48024]`, `dark:[--color-link:#e18431]`)}
|
||||||
darkColor="#e18431"
|
|
||||||
>
|
>
|
||||||
vanilla JavaScript
|
vanilla JavaScript
|
||||||
</Link>
|
</Link>
|
||||||
), too.
|
), too.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
|
||||||
Whenever possible, I also apply my experience in{" "}
|
Whenever possible, I also apply my experience in{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://bugcrowd.com/jakejarvis"
|
href="https://bugcrowd.com/jakejarvis"
|
||||||
title="Jake Jarvis on Bugcrowd"
|
title="Jake Jarvis on Bugcrowd"
|
||||||
lightColor="#00b81a"
|
className={cn(`[--color-link:#00b81a]`, `dark:[--color-link:#57f06d]`)}
|
||||||
darkColor="#57f06d"
|
|
||||||
>
|
>
|
||||||
application security
|
application security
|
||||||
</Link>
|
</Link>
|
||||||
@ -129,8 +80,7 @@ const Page = () => {
|
|||||||
<Link
|
<Link
|
||||||
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
|
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
|
||||||
title='"What is serverless computing?" on Cloudflare'
|
title='"What is serverless computing?" on Cloudflare'
|
||||||
lightColor="#0098ec"
|
className={cn(`[--color-link:#0098ec]`, `dark:[--color-link:#43b9fb]`)}
|
||||||
darkColor="#43b9fb"
|
|
||||||
>
|
>
|
||||||
serverless stacks
|
serverless stacks
|
||||||
</Link>
|
</Link>
|
||||||
@ -138,21 +88,19 @@ const Page = () => {
|
|||||||
<Link
|
<Link
|
||||||
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=source&language=&sort=stargazers"
|
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=source&language=&sort=stargazers"
|
||||||
title='My repositories tagged with "github-actions" on GitHub'
|
title='My repositories tagged with "github-actions" on GitHub'
|
||||||
lightColor="#ff6200"
|
className={cn(`[--color-link:#ff6200]`, `dark:[--color-link:#f46c16]`)}
|
||||||
darkColor="#f46c16"
|
|
||||||
>
|
>
|
||||||
DevOps automation
|
DevOps automation
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
|
||||||
I fell in love with{" "}
|
I fell in love with{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/previously"
|
href="/previously"
|
||||||
title="My Terrible, Horrible, No Good, Very Bad First Websites"
|
title="My Terrible, Horrible, No Good, Very Bad First Websites"
|
||||||
lightColor="#4169e1"
|
className={cn(`[--color-link:#4169e1]`, `dark:[--color-link:#8ca9ff]`)}
|
||||||
darkColor="#8ca9ff"
|
|
||||||
>
|
>
|
||||||
frontend web design
|
frontend web design
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
@ -160,8 +108,7 @@ const Page = () => {
|
|||||||
<Link
|
<Link
|
||||||
href="/notes/my-first-code"
|
href="/notes/my-first-code"
|
||||||
title="Jake's Bulletin Board, circa 2003"
|
title="Jake's Bulletin Board, circa 2003"
|
||||||
lightColor="#9932cc"
|
className={cn(`[--color-link:#9932cc]`, `dark:[--color-link:#d588fb]`)}
|
||||||
darkColor="#d588fb"
|
|
||||||
>
|
>
|
||||||
backend programming
|
backend programming
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
@ -169,86 +116,76 @@ const Page = () => {
|
|||||||
<Link
|
<Link
|
||||||
href="/birthday"
|
href="/birthday"
|
||||||
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
||||||
lightColor="#e40088"
|
className={cn(`[--color-link:#e40088]`, `dark:[--color-link:#fd40b1]`)}
|
||||||
darkColor="#fd40b1"
|
|
||||||
style={{
|
style={{
|
||||||
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`,
|
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`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
the Tooth Fairy
|
the Tooth Fairy
|
||||||
</Link>
|
</Link>
|
||||||
. <span style={{ color: "var(--colors-medium-light)" }}>I’ve improved a bit since then, I think? 🤷</span>
|
. <span className="text-medium-light">I’ve improved a bit since then, I think? 🤷</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
|
||||||
Over the years, some of my side projects{" "}
|
Over the years, some of my side projects{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/leo"
|
href="/leo"
|
||||||
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
|
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
|
||||||
lightColor="#ff1b1b"
|
className={cn(`[--color-link:#ff1b1b]`, `dark:[--color-link:#f06060]`)}
|
||||||
darkColor="#f06060"
|
|
||||||
>
|
>
|
||||||
have
|
have
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
|
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
|
||||||
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
|
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
|
||||||
lightColor="#f78200"
|
className={cn(`[--color-link:#f78200]`, `dark:[--color-link:#fd992a]`)}
|
||||||
darkColor="#fd992a"
|
|
||||||
>
|
>
|
||||||
been
|
been
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<Link
|
<Link
|
||||||
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"
|
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)'
|
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
|
||||||
lightColor="#f2b702"
|
className={cn(`[--color-link:#f2b702]`, `dark:[--color-link:#ffcc2e]`)}
|
||||||
darkColor="#ffcc2e"
|
|
||||||
>
|
>
|
||||||
featured
|
featured
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
|
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
|
||||||
title='"The new Facebook is on a roll" on CNN Money'
|
title='"The new Facebook is on a roll" on CNN Money'
|
||||||
lightColor="#5ebd3e"
|
className={cn(`[--color-link:#5ebd3e]`, `dark:[--color-link:#78df55]`)}
|
||||||
darkColor="#78df55"
|
|
||||||
>
|
>
|
||||||
by
|
by
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://www.wired.com/2007/04/our-web-servers/"
|
href="https://www.wired.com/2007/04/our-web-servers/"
|
||||||
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
|
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
|
||||||
lightColor="#009cdf"
|
className={cn(`[--color-link:#009cdf]`, `dark:[--color-link:#29bfff]`)}
|
||||||
darkColor="#29bfff"
|
|
||||||
>
|
>
|
||||||
various
|
various
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
|
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'
|
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
|
||||||
lightColor="#3e49bb"
|
className={cn(`[--color-link:#3e49bb]`, `dark:[--color-link:#7b87ff]`)}
|
||||||
darkColor="#7b87ff"
|
|
||||||
>
|
>
|
||||||
media
|
media
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
|
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
|
||||||
title='"Your Next Client? The CEO's Son" on Advertising Age'
|
title='"Your Next Client? The CEO's Son" on Advertising Age'
|
||||||
lightColor="#973999"
|
className={cn(`[--color-link:#973999]`, `dark:[--color-link:#db60dd]`)}
|
||||||
darkColor="#db60dd"
|
|
||||||
>
|
>
|
||||||
outlets
|
outlets
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p className="text-base leading-7 md:text-[0.975rem]">
|
||||||
You can find my work on{" "}
|
You can find my work on{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://github.com/jakejarvis"
|
href="https://github.com/jakejarvis"
|
||||||
rel="me"
|
rel="me"
|
||||||
title="Jake Jarvis on GitHub"
|
className={cn(`[--color-link:#8d4eff]`, `dark:[--color-link:#a379f0]`)}
|
||||||
lightColor="#8d4eff"
|
|
||||||
darkColor="#a379f0"
|
|
||||||
>
|
>
|
||||||
GitHub
|
GitHub
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
@ -256,44 +193,34 @@ const Page = () => {
|
|||||||
<Link
|
<Link
|
||||||
href="https://www.linkedin.com/in/jakejarvis/"
|
href="https://www.linkedin.com/in/jakejarvis/"
|
||||||
rel="me"
|
rel="me"
|
||||||
title="Jake Jarvis on LinkedIn"
|
className={cn(`[--color-link:#0073b1]`, `dark:[--color-link:#3b9dd2]`)}
|
||||||
lightColor="#0073b1"
|
|
||||||
darkColor="#3b9dd2"
|
|
||||||
>
|
>
|
||||||
LinkedIn
|
LinkedIn
|
||||||
</Link>
|
</Link>
|
||||||
. I’m always available to connect over{" "}
|
. I’m always available to connect over{" "}
|
||||||
<Link href="/contact" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
|
<Link
|
||||||
|
href="/contact"
|
||||||
|
title="Send an email"
|
||||||
|
className={cn(`[--color-link:#de0c0c]`, `dark:[--color-link:#ff5050]`)}
|
||||||
|
>
|
||||||
email
|
email
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<sup>
|
<sup className="mx-0.5 text-[0.6rem]">
|
||||||
<Link
|
<Link
|
||||||
href="https://jrvs.io/pgp"
|
href="https://jrvs.io/pgp"
|
||||||
rel="pgpkey"
|
rel="pgpkey"
|
||||||
title="My Public Key"
|
title="My Public Key"
|
||||||
lightColor="#757575"
|
className={cn(`[--color-link:#757575]`, `dark:[--color-link:#959595]`, "hover:no-underline")}
|
||||||
darkColor="#959595"
|
|
||||||
plain
|
|
||||||
>
|
>
|
||||||
<LockIcon size="1.25em" style={{ verticalAlign: "text-top" }} />{" "}
|
<LockIcon size="1.25em" className="inline align-text-top" />{" "}
|
||||||
<code
|
<code className="mx-0.5 tracking-wider [word-spacing:-4px]">2B0C 9CF2 51E6 9A39</code>
|
||||||
style={{
|
|
||||||
margin: "0 0.15em",
|
|
||||||
letterSpacing: "0.075em",
|
|
||||||
wordSpacing: "-0.4em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
2B0C 9CF2 51E6 9A39
|
|
||||||
</code>
|
|
||||||
</Link>
|
</Link>
|
||||||
</sup>
|
</sup>
|
||||||
,{" "}
|
,{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://bsky.app/profile/jarv.is"
|
href="https://bsky.app/profile/jarv.is"
|
||||||
rel="me"
|
rel="me"
|
||||||
title="Jake Jarvis on Bluesky"
|
className={cn(`[--color-link:#0085ff]`, `dark:[--color-link:#208bfe]`)}
|
||||||
lightColor="#0085ff"
|
|
||||||
darkColor="#208bfe"
|
|
||||||
>
|
>
|
||||||
Bluesky
|
Bluesky
|
||||||
</Link>
|
</Link>
|
||||||
@ -301,15 +228,13 @@ const Page = () => {
|
|||||||
<Link
|
<Link
|
||||||
href="https://fediverse.jarv.is/@jake"
|
href="https://fediverse.jarv.is/@jake"
|
||||||
rel="me"
|
rel="me"
|
||||||
title="Jake Jarvis on Mastodon"
|
className={cn(`[--color-link:#6d6eff]`, `dark:[--color-link:#7b87ff]`)}
|
||||||
lightColor="#6d6eff"
|
|
||||||
darkColor="#7b87ff"
|
|
||||||
>
|
>
|
||||||
Mastodon
|
Mastodon
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
as well!
|
as well!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
.heading {
|
|
||||||
font-size: 1.4em;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar {
|
|
||||||
--activity-0: #ebedf0;
|
|
||||||
--activity-1: #9be9a8;
|
|
||||||
--activity-2: #40c463;
|
|
||||||
--activity-3: #30a14e;
|
|
||||||
--activity-4: #216e39;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .calendar {
|
|
||||||
--activity-0: #252525;
|
|
||||||
--activity-1: #033a16;
|
|
||||||
--activity-2: #196c2e;
|
|
||||||
--activity-3: #2ea043;
|
|
||||||
--activity-4: #56d364;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar :global(.react-activity-calendar) {
|
|
||||||
margin: 1em auto 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar :global(.react-activity-calendar__count),
|
|
||||||
.calendar :global(.react-activity-calendar__legend-month) {
|
|
||||||
color: var(--colors-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar :global(.react-activity-calendar__legend-colors) {
|
|
||||||
color: var(--colors-medium-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
line-height: 1.1;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 370px;
|
|
||||||
padding: 1.2em 1.2em 0.8em;
|
|
||||||
border: 1px solid var(--colors-kinda-light);
|
|
||||||
border-radius: 1em;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--colors-medium-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .name {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0.4em;
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .description {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .metaItem {
|
|
||||||
margin: 0.3em 1.5em 0.3em 0;
|
|
||||||
color: var(--colors-medium);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .metaLink {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .metaLink:hover,
|
|
||||||
.card .metaLink:focus-visible {
|
|
||||||
color: var(--colors-link);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .metaIcon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1.25em;
|
|
||||||
height: 1.25em;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
@ -6,11 +6,10 @@ import Calendar from "./calendar";
|
|||||||
import PageTitle from "../../components/PageTitle";
|
import PageTitle from "../../components/PageTitle";
|
||||||
import Link from "../../components/Link";
|
import Link from "../../components/Link";
|
||||||
import RelativeTime from "../../components/RelativeTime";
|
import RelativeTime from "../../components/RelativeTime";
|
||||||
|
import cn from "../../lib/helpers/classnames";
|
||||||
import { createMetadata } from "../../lib/helpers/metadata";
|
import { createMetadata } from "../../lib/helpers/metadata";
|
||||||
import { getContributions, getRepos } from "./github";
|
import { getContributions, getRepos } from "./github";
|
||||||
|
|
||||||
import styles from "./page.module.css";
|
|
||||||
|
|
||||||
export const metadata = createMetadata({
|
export const metadata = createMetadata({
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
description: `Most-starred repositories by @${env.NEXT_PUBLIC_GITHUB_USERNAME} on GitHub`,
|
description: `Most-starred repositories by @${env.NEXT_PUBLIC_GITHUB_USERNAME} on GitHub`,
|
||||||
@ -32,41 +31,55 @@ const Page = async () => {
|
|||||||
<>
|
<>
|
||||||
<PageTitle canonical="/projects">Projects</PageTitle>
|
<PageTitle canonical="/projects">Projects</PageTitle>
|
||||||
|
|
||||||
<h2 className={styles.heading}>
|
<h2 className="my-3.5 text-[1.4em] font-normal">
|
||||||
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`} style={{ color: "inherit" }} plain>
|
<Link
|
||||||
|
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}
|
||||||
|
className="text-inherit hover:no-underline"
|
||||||
|
>
|
||||||
Contribution activity
|
Contribution activity
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Suspense fallback={<p>Failed to generate activity calendar.</p>}>
|
<Suspense fallback={<p>Failed to generate activity calendar.</p>}>
|
||||||
<Calendar data={contributions} className={styles.calendar} />
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mx-auto mt-4 mb-8",
|
||||||
|
"[--activity-0:#ebedf0] [--activity-1:#9be9a8] [--activity-2:#40c463] [--activity-3:#30a14e] [--activity-4:#216e39]",
|
||||||
|
"dark:[--activity-0:#252525] dark:[--activity-1:#033a16] dark:[--activity-2:#196c2e] dark:[--activity-3:#2ea043] dark:[--activity-4:#56d364]",
|
||||||
|
String.raw`[&_.react-activity-calendar\_\_count]:text-medium [&_.react-activity-calendar\_\_legend-month]:text-medium [&_.react-activity-calendar\_\_legend-colors]:text-medium-light`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Calendar data={contributions} />
|
||||||
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<h2 className={styles.heading}>
|
<h2 className="my-3.5 text-[1.4em] font-normal">
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`}
|
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`}
|
||||||
style={{ color: "inherit" }}
|
className="text-inherit hover:no-underline"
|
||||||
plain
|
|
||||||
>
|
>
|
||||||
Popular repositories
|
Popular repositories
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className={styles.grid}>
|
<div className="flex w-full flex-row flex-wrap items-start justify-between gap-[1em] leading-[1.1]">
|
||||||
{repos?.map((repo) => (
|
{repos?.map((repo) => (
|
||||||
<div key={repo!.name} className={styles.card}>
|
<div
|
||||||
<Link href={repo!.url} className={styles.name}>
|
key={repo!.name}
|
||||||
|
className="border-kinda-light text-medium-dark w-[370px] grow rounded-[1em] border border-solid px-[1.2em] pt-[1.2em] pb-[0.8em] text-[0.9em]"
|
||||||
|
>
|
||||||
|
<Link href={repo!.url} className="mb-[0.4em] inline-block text-[1.2em] font-semibold">
|
||||||
{repo!.name}
|
{repo!.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{repo!.description && <p className={styles.description}>{repo!.description}</p>}
|
{repo!.description && <p className="m-0 leading-[1.7]">{repo!.description}</p>}
|
||||||
|
|
||||||
<div className={styles.meta}>
|
<div className="mt-[0.4em] flex flex-wrap">
|
||||||
{repo!.primaryLanguage && (
|
{repo!.primaryLanguage && (
|
||||||
<div className={styles.metaItem}>
|
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
|
||||||
{repo!.primaryLanguage.color && (
|
{repo!.primaryLanguage.color && (
|
||||||
<span
|
<span
|
||||||
className={styles.metaIcon}
|
className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top"
|
||||||
style={{ backgroundColor: repo!.primaryLanguage.color, borderRadius: "50%" }}
|
style={{ backgroundColor: repo!.primaryLanguage.color, borderRadius: "50%" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -75,41 +88,38 @@ const Page = async () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{repo!.stargazerCount > 0 && (
|
{repo!.stargazerCount > 0 && (
|
||||||
<div className={styles.metaItem}>
|
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
|
||||||
<Link
|
<Link
|
||||||
href={`${repo!.url}/stargazers`}
|
href={`${repo!.url}/stargazers`}
|
||||||
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)} ${repo!.stargazerCount === 1 ? "star" : "stars"}`}
|
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)} ${repo!.stargazerCount === 1 ? "star" : "stars"}`}
|
||||||
plain
|
className="hover:text-link text-inherit hover:no-underline"
|
||||||
className={styles.metaLink}
|
|
||||||
>
|
>
|
||||||
<StarIcon size="1.25em" className={styles.metaIcon} />
|
<StarIcon size="1.25em" className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top" />
|
||||||
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)}</span>
|
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{repo!.forkCount > 0 && (
|
{repo!.forkCount > 0 && (
|
||||||
<div className={styles.metaItem}>
|
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
|
||||||
<Link
|
<Link
|
||||||
href={`${repo!.url}/network/members`}
|
href={`${repo!.url}/network/members`}
|
||||||
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)} ${repo!.forkCount === 1 ? "fork" : "forks"}`}
|
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)} ${repo!.forkCount === 1 ? "fork" : "forks"}`}
|
||||||
plain
|
className="hover:text-link text-inherit hover:no-underline"
|
||||||
className={styles.metaLink}
|
|
||||||
>
|
>
|
||||||
<GitForkIcon size="1.25em" className={styles.metaIcon} />
|
<GitForkIcon
|
||||||
|
size="1.25em"
|
||||||
|
className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top"
|
||||||
|
/>
|
||||||
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)}</span>
|
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.metaItem}>
|
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
className={styles.metaIcon}
|
// invisible icon hack to fix line height
|
||||||
style={{
|
className="mr-0 inline-block h-[1.25em] w-0 align-text-top"
|
||||||
// invisible icon hack to fix line height
|
|
||||||
width: 0,
|
|
||||||
marginRight: 0,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Updated <RelativeTime date={repo!.pushedAt} />
|
Updated <RelativeTime date={repo!.pushedAt} />
|
||||||
@ -120,13 +130,7 @@ const Page = async () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p className="mt-4 mb-0 text-center text-base font-medium">
|
||||||
style={{
|
|
||||||
textAlign: "center",
|
|
||||||
marginBottom: 0,
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}>
|
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}>
|
||||||
View more on{" "}
|
View more on{" "}
|
||||||
<svg
|
<svg
|
||||||
@ -134,13 +138,7 @@ const Page = async () => {
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
width="1.2em"
|
width="1.2em"
|
||||||
height="1.2em"
|
height="1.2em"
|
||||||
style={{
|
className="fill-text mr-[0.1em] ml-[0.25em] inline h-[1.2em] w-[1.2em] align-text-top"
|
||||||
width: "1.2em",
|
|
||||||
height: "1.2em",
|
|
||||||
verticalAlign: "text-top",
|
|
||||||
margin: "0 0.1em 0 0.25em",
|
|
||||||
fill: "var(--colors-text)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
|
@ -45,7 +45,7 @@ const Page = () => {
|
|||||||
>
|
>
|
||||||
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
|
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
|
||||||
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>mv</span> /root
|
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>mv</span> /root
|
||||||
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} plain>
|
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} className="hover:no-underline">
|
||||||
/stable_products_that_people_rely_on/
|
/stable_products_that_people_rely_on/
|
||||||
</Link>
|
</Link>
|
||||||
googledomains.zip /tmp/
|
googledomains.zip /tmp/
|
||||||
@ -64,7 +64,7 @@ const Page = () => {
|
|||||||
<br />
|
<br />
|
||||||
<span style={{ color: "#78df55" }}>@monthly</span>
|
<span style={{ color: "#78df55" }}>@monthly</span>
|
||||||
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span> /tmp/
|
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span> /tmp/
|
||||||
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} plain>
|
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} className="hover:no-underline">
|
||||||
*.zip
|
*.zip
|
||||||
</Link>
|
</Link>
|
||||||
<br />
|
<br />
|
||||||
|
@ -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);
|
|
||||||
}
|
|
@ -1,12 +1,10 @@
|
|||||||
import clsx from "clsx";
|
import cn from "../../lib/helpers/classnames";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
|
|
||||||
import styles from "./Blockquote.module.css";
|
|
||||||
|
|
||||||
export type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">;
|
export type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">;
|
||||||
|
|
||||||
const Blockquote = ({ className, ...rest }: BlockquoteProps) => (
|
const Blockquote = ({ className, ...rest }: BlockquoteProps) => (
|
||||||
<blockquote className={clsx(styles.blockquote, className)} {...rest} />
|
<blockquote className={cn("border-l-link text-medium-dark ml-0 border-l-4 pl-5", className)} {...rest} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Blockquote;
|
export default Blockquote;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +1,44 @@
|
|||||||
import { env } from "../../lib/env";
|
import { env } from "../../lib/env";
|
||||||
import clsx from "clsx";
|
|
||||||
import { HeartIcon } from "lucide-react";
|
import { HeartIcon } from "lucide-react";
|
||||||
import Link from "../Link";
|
import Link from "../Link";
|
||||||
|
import cn from "../../lib/helpers/classnames";
|
||||||
import * as config from "../../lib/config";
|
import * as config from "../../lib/config";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
|
|
||||||
import styles from "./Footer.module.css";
|
|
||||||
|
|
||||||
export type FooterProps = ComponentPropsWithoutRef<"footer">;
|
export type FooterProps = ComponentPropsWithoutRef<"footer">;
|
||||||
|
|
||||||
const Footer = ({ className, ...rest }: FooterProps) => {
|
const Footer = ({ className, ...rest }: FooterProps) => {
|
||||||
return (
|
return (
|
||||||
<footer className={clsx(styles.footer, className)} {...rest}>
|
<footer
|
||||||
<div className={styles.row}>
|
className={cn("border-t-kinda-light bg-background-outer text-medium-dark w-full border-t py-4", className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<div className="max-w-default mx-auto flex w-full flex-col justify-between px-5 text-[0.8rem] leading-9 md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
Content{" "}
|
Content{" "}
|
||||||
<Link href="/license" title={config.license} plain className={styles.link}>
|
<Link href="/license" title={config.license} className="text-medium-dark hover:no-underline">
|
||||||
licensed under {config.licenseAbbr}
|
licensed under {config.licenseAbbr}
|
||||||
</Link>
|
</Link>
|
||||||
,{" "}
|
,{" "}
|
||||||
<Link href="/previously" title="Previously on..." plain className={styles.link}>
|
<Link href="/previously" title="Previously on..." className="text-medium-dark hover:no-underline">
|
||||||
{config.copyrightYearStart}
|
{config.copyrightYearStart}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
– {new Date().getUTCFullYear()}.
|
– {new Date().getUTCFullYear()}.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Made with <HeartIcon size="1.25em" fill="currentColor" className={clsx(styles.icon, styles.heart)} /> and{" "}
|
Made with{" "}
|
||||||
|
<HeartIcon
|
||||||
|
size="1.25em"
|
||||||
|
fill="currentColor"
|
||||||
|
className="animate-heartbeat text-error mx-0.25 inline h-[1.25em] w-[1.25em] align-text-top"
|
||||||
|
/>{" "}
|
||||||
|
and{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://nextjs.org/"
|
href="https://nextjs.org/"
|
||||||
title="Powered by Next.js"
|
title="Powered by Next.js"
|
||||||
aria-label="Next.js"
|
aria-label="Next.js"
|
||||||
plain
|
className="text-medium-dark hover:text-medium hover:no-underline"
|
||||||
className={styles.link}
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -42,7 +48,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
width="1.25em"
|
width="1.25em"
|
||||||
height="1.25em"
|
height="1.25em"
|
||||||
className={styles.icon}
|
className="mx-0.25 inline h-[1.25em] w-[1.25em] align-text-top"
|
||||||
>
|
>
|
||||||
<path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" />
|
<path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -51,8 +57,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
|
|||||||
<Link
|
<Link
|
||||||
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`}
|
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`}
|
||||||
title="View Source on GitHub"
|
title="View Source on GitHub"
|
||||||
plain
|
className="border-b-light text-medium-dark hover:border-b-kinda-light border-b-2 pb-0.5 hover:no-underline"
|
||||||
className={clsx(styles.link, styles.underline)}
|
|
||||||
>
|
>
|
||||||
View source.
|
View source.
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
.header {
|
|
||||||
width: 100%;
|
|
||||||
height: 4.5em;
|
|
||||||
padding: 0.7em 1.5em;
|
|
||||||
border-bottom: 1px solid var(--colors-kinda-light);
|
|
||||||
background-color: var(--colors-background-header);
|
|
||||||
|
|
||||||
/* make sticky */
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
/* blurry glass-like background effect (except on firefox...?) */
|
|
||||||
backdrop-filter: saturate(180%) blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border: 1px solid var(--colors-light);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--colors-medium-dark) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home:hover,
|
|
||||||
.home:focus-visible {
|
|
||||||
color: var(--colors-link) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
margin: 0 0.6em;
|
|
||||||
font-size: 1.15em;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
max-width: var(--max-width);
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.header {
|
|
||||||
padding: 0.75em 1.25em;
|
|
||||||
height: 5.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home:hover .avatar,
|
|
||||||
.home:focus-visible .avatar {
|
|
||||||
border-color: var(--colors-link-underline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
max-width: 325px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 380px) {
|
|
||||||
.menu {
|
|
||||||
max-width: 225px;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +1,46 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import clsx from "clsx";
|
|
||||||
import Link from "../Link";
|
import Link from "../Link";
|
||||||
import Menu from "../Menu";
|
import Menu from "../Menu";
|
||||||
|
import cn from "../../lib/helpers/classnames";
|
||||||
import * as config from "../../lib/config";
|
import * as config from "../../lib/config";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
|
|
||||||
import styles from "./Header.module.css";
|
|
||||||
|
|
||||||
import avatarImg from "../../app/avatar.jpg";
|
import avatarImg from "../../app/avatar.jpg";
|
||||||
|
|
||||||
export type HeaderProps = ComponentPropsWithoutRef<"header">;
|
export type HeaderProps = ComponentPropsWithoutRef<"header">;
|
||||||
|
|
||||||
const Header = ({ className, ...rest }: HeaderProps) => {
|
const Header = ({ className, ...rest }: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<header className={clsx(styles.header, className)} {...rest}>
|
<header
|
||||||
<nav className={styles.nav}>
|
className={cn(
|
||||||
<Link dynamicOnHover href="/" rel="author" aria-label={config.authorName} plain className={styles.home}>
|
"bg-background-outer/70 border-kinda-light sticky top-0 z-[1000] h-24 w-full border-b backdrop-blur-[5px] backdrop-saturate-[180%] md:h-18",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<nav className="max-w-default mx-auto flex h-full w-full items-center justify-between px-5 py-3 md:py-5">
|
||||||
|
<Link
|
||||||
|
dynamicOnHover
|
||||||
|
href="/"
|
||||||
|
rel="author"
|
||||||
|
aria-label={config.authorName}
|
||||||
|
className="text-medium-dark hover:text-link flex flex-shrink-0 items-center hover:no-underline"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src={avatarImg}
|
src={avatarImg}
|
||||||
alt={`Photo of ${config.authorName}`}
|
alt={`Photo of ${config.authorName}`}
|
||||||
className={styles.avatar}
|
className="border-light h-[70px] w-[70px] rounded-full border-2 md:h-[48px] md:w-[48px] md:border"
|
||||||
width={70}
|
width={70}
|
||||||
height={70}
|
height={70}
|
||||||
quality={50}
|
quality={50}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<span className={styles.name}>{config.authorName}</span>
|
<span className="mx-2.5 hidden text-lg leading-none font-medium tracking-[0.02em] md:block">
|
||||||
|
{config.authorName}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Menu className={styles.menu} />
|
<Menu className="ml-6 w-full max-w-64 sm:ml-4 sm:max-w-96 md:ml-0 md:max-w-none" />
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ export type HeadingAnchorProps = Omit<ComponentPropsWithoutRef<typeof Link>, "hr
|
|||||||
|
|
||||||
const HeadingAnchor = ({ id, ...rest }: HeadingAnchorProps) => {
|
const HeadingAnchor = ({ id, ...rest }: HeadingAnchorProps) => {
|
||||||
return (
|
return (
|
||||||
<Link href={`#${id}`} plain {...rest}>
|
<Link href={`#${id}`} className="hover:no-underline" {...rest}>
|
||||||
<LinkIcon size="0.8em" />
|
<LinkIcon size="0.8em" />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
.link {
|
|
||||||
color: var(--colors-link);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fancy underline effect on hover */
|
|
||||||
.link:not(.plain) {
|
|
||||||
background-image: linear-gradient(var(--colors-link-underline), var(--colors-link-underline));
|
|
||||||
background-position: 0% 100%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 0% 2px;
|
|
||||||
transition: background-size 0.2s ease-in-out;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link:not(.plain):hover,
|
|
||||||
.link:not(.plain):focus-visible {
|
|
||||||
background-size: 100% 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.link:not(.plain) {
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +1,13 @@
|
|||||||
import NextLink from "next/link";
|
import NextLink from "next/link";
|
||||||
import clsx from "clsx";
|
import cn from "../../lib/helpers/classnames";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
|
|
||||||
import styles from "./Link.module.css";
|
|
||||||
|
|
||||||
export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & {
|
export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & {
|
||||||
/** Disables fancy text-decoration effect when true. */
|
|
||||||
plain?: boolean;
|
|
||||||
|
|
||||||
// https://github.com/vercel/next.js/pull/77866/files#diff-040f76a8f302dd3a8ec7de0867048475271f052b094cd73d2d0751b495c02f7dR30
|
// https://github.com/vercel/next.js/pull/77866/files#diff-040f76a8f302dd3a8ec7de0867048475271f052b094cd73d2d0751b495c02f7dR30
|
||||||
dynamicOnHover?: boolean;
|
dynamicOnHover?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, plain, className, ...rest }: LinkProps) => {
|
const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, className, ...rest }: LinkProps) => {
|
||||||
// This component auto-detects whether or not this link should open in the same window (the default for internal
|
// This component auto-detects whether or not this link should open in the same window (the default for internal
|
||||||
// links) or a new tab (the default for external links). Defaults can be overridden with `target="_blank"`.
|
// links) or a new tab (the default for external links). Defaults can be overridden with `target="_blank"`.
|
||||||
const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]);
|
const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]);
|
||||||
@ -25,10 +20,8 @@ const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, plain, clas
|
|||||||
href={href}
|
href={href}
|
||||||
target={target || (isExternal ? "_blank" : undefined)}
|
target={target || (isExternal ? "_blank" : undefined)}
|
||||||
rel={`${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}` || undefined}
|
rel={`${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}` || undefined}
|
||||||
className={clsx(
|
className={cn(
|
||||||
styles.link,
|
"text-link hover:decoration-link/40 hover:underline hover:decoration-2 hover:underline-offset-4",
|
||||||
// eslint-disable-next-line css-modules/no-undef-class
|
|
||||||
plain && styles.plain,
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
.menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: block;
|
|
||||||
margin-left: 1em;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.menu {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 380px) {
|
|
||||||
.menu {
|
|
||||||
margin-left: 1.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* the home icon is kinda redundant when space is SUPER tight */
|
|
||||||
.item:first-of-type {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +1,31 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import clsx from "clsx";
|
|
||||||
import MenuItem from "../MenuItem";
|
import MenuItem from "../MenuItem";
|
||||||
import ThemeToggle from "../ThemeToggle";
|
import ThemeToggle from "../ThemeToggle";
|
||||||
|
import cn from "../../lib/helpers/classnames";
|
||||||
import { menuItems } from "../../lib/config/menu";
|
import { menuItems } from "../../lib/config/menu";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
|
|
||||||
import styles from "./Menu.module.css";
|
|
||||||
|
|
||||||
export type MenuProps = ComponentPropsWithoutRef<"ul">;
|
export type MenuProps = ComponentPropsWithoutRef<"ul">;
|
||||||
|
|
||||||
const Menu = ({ className, ...rest }: MenuProps) => {
|
const Menu = ({ className, ...rest }: MenuProps) => {
|
||||||
const pathname = usePathname() || "";
|
const pathname = usePathname() || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={clsx(styles.menu, className)} {...rest}>
|
<ul className={cn("flex flex-row justify-between md:justify-end", className)} {...rest}>
|
||||||
{menuItems.map((item, index) => {
|
{menuItems.map((item, index) => {
|
||||||
// kinda weird/hacky way to determine if the *first part* of the current path matches this href
|
// kinda weird/hacky way to determine if the *first part* of the current path matches this href
|
||||||
const isCurrent = item.href === `/${pathname.split("/")[1]}`;
|
const isCurrent = item.href === `/${pathname.split("/")[1]}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={styles.item} key={item.text || index}>
|
<li className="first-of-type:hidden sm:first-of-type:block md:ml-4" key={item.text || index}>
|
||||||
<MenuItem {...item} current={isCurrent} />
|
<MenuItem {...item} current={isCurrent} />
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<li
|
<li className="-mr-2.5 md:ml-4">
|
||||||
className={styles.item}
|
|
||||||
style={{
|
|
||||||
// manually align the theme toggle with the rest of the menu icons
|
|
||||||
paddingTop: "0.2em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
icon={ThemeToggle}
|
icon={ThemeToggle}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
.link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.6em;
|
|
||||||
margin-top: 0.2em;
|
|
||||||
color: var(--colors-medium-dark) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* indicate active page/section */
|
|
||||||
.link.current {
|
|
||||||
margin-bottom: -0.2em;
|
|
||||||
border-bottom: 0.2em solid var(--colors-link-underline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link:not(.current):hover,
|
|
||||||
.link:not(.current):focus-visible {
|
|
||||||
margin-bottom: -0.2em;
|
|
||||||
border-bottom: 0.2em solid var(--colors-kinda-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
display: block;
|
|
||||||
width: 1.25em;
|
|
||||||
height: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-left: 0.7em;
|
|
||||||
font-size: 0.925em;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.025em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.icon {
|
|
||||||
width: 1.8em;
|
|
||||||
height: 1.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,8 @@
|
|||||||
import clsx from "clsx";
|
import cn from "../../lib/helpers/classnames";
|
||||||
import Link from "../Link";
|
import Link from "../Link";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
import styles from "./MenuItem.module.css";
|
|
||||||
|
|
||||||
export type MenuItemProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href"> & {
|
export type MenuItemProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href"> & {
|
||||||
text?: string;
|
text?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
@ -17,8 +15,8 @@ const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemPro
|
|||||||
|
|
||||||
const item = (
|
const item = (
|
||||||
<>
|
<>
|
||||||
{Icon && <Icon size="1.25em" className={styles.icon} />}
|
{Icon && <Icon size="1.25em" className="block h-[1.8em] w-[1.8em] md:h-[1.25em] md:w-[1.25em]" />}
|
||||||
{text && <span className={styles.label}>{text}</span>}
|
{text && <span className="ml-3 hidden text-sm leading-none font-medium tracking-[0.02em] md:block">{text}</span>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -29,8 +27,12 @@ const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemPro
|
|||||||
dynamicOnHover
|
dynamicOnHover
|
||||||
href={href}
|
href={href}
|
||||||
aria-label={text}
|
aria-label={text}
|
||||||
plain
|
data-current={current || undefined}
|
||||||
className={clsx(styles.link, current && styles.current, className)}
|
className={cn(
|
||||||
|
"text-medium-dark hover:border-kinda-light -mb-[0.2em] inline-flex items-center p-2.5 hover:border-b-[0.2em] hover:no-underline",
|
||||||
|
current && "border-link/40 hover:border-link/40 border-b-[0.2em]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{item}
|
{item}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
.title {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0.6em;
|
|
||||||
font-size: 2em;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: left;
|
|
||||||
text-transform: lowercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slug::before {
|
|
||||||
content: "\002E\002F"; /* "./" */
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
color: var(--colors-medium-light);
|
|
||||||
margin-right: -0.1em;
|
|
||||||
}
|
|
@ -1,17 +1,18 @@
|
|||||||
import clsx from "clsx";
|
import cn from "../../lib/helpers/classnames";
|
||||||
import Link from "../Link";
|
import Link from "../Link";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
|
|
||||||
import styles from "./PageTitle.module.css";
|
|
||||||
|
|
||||||
export type PageTitleProps = ComponentPropsWithoutRef<"h1"> & {
|
export type PageTitleProps = ComponentPropsWithoutRef<"h1"> & {
|
||||||
canonical: string;
|
canonical: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageTitle = ({ canonical, className, children, ...rest }: PageTitleProps) => {
|
const PageTitle = ({ canonical, className, children, ...rest }: PageTitleProps) => {
|
||||||
return (
|
return (
|
||||||
<h1 className={clsx(styles.title, className)} {...rest}>
|
<h1 className={cn("mt-1 mb-6 text-left text-3xl font-medium lowercase", className)} {...rest}>
|
||||||
<Link href={canonical} plain className={styles.slug}>
|
<Link
|
||||||
|
href={canonical}
|
||||||
|
className="before:text-medium-light before:mr-[-0.1em] before:tracking-widest before:content-['\002E\002F']"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
.toggle {
|
|
||||||
display: block;
|
|
||||||
border: 0;
|
|
||||||
padding: 0.6em;
|
|
||||||
margin-right: -0.6em;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--colors-medium-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle:hover,
|
|
||||||
.toggle:focus-visible {
|
|
||||||
color: var(--colors-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* hacky way to avoid flashing icon for a few milliseconds on initial render */
|
|
||||||
.toggle > .sun,
|
|
||||||
[data-theme="dark"] .toggle > .moon {
|
|
||||||
display: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stylelint-disable-next-line no-descending-specificity */
|
|
||||||
.toggle > .moon,
|
|
||||||
[data-theme="dark"] .toggle > .sun {
|
|
||||||
display: none;
|
|
||||||
}
|
|
@ -6,8 +6,6 @@ import { useTheme } from "../../hooks";
|
|||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
import styles from "./ThemeToggle.module.css";
|
|
||||||
|
|
||||||
export type ThemeToggleProps = ComponentPropsWithoutRef<LucideIcon>;
|
export type ThemeToggleProps = ComponentPropsWithoutRef<LucideIcon>;
|
||||||
|
|
||||||
const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
|
const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
|
||||||
@ -17,10 +15,10 @@ const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||||
aria-label="Toggle Theme"
|
aria-label="Toggle Theme"
|
||||||
className={styles.toggle}
|
className="hover:text-warning block bg-none p-2.5 hover:cursor-pointer hover:border-none"
|
||||||
>
|
>
|
||||||
<SunIcon className={clsx(styles.sun, className)} {...rest} />
|
<SunIcon className={clsx("!block dark:!hidden", className)} {...rest} />
|
||||||
<MoonIcon className={clsx(styles.moon, className)} {...rest} />
|
<MoonIcon className={clsx("!hidden dark:!block", className)} {...rest} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
export const POSTS_DIR = "notes";
|
export const POSTS_DIR = "notes";
|
||||||
|
|
||||||
/** Maximum width of content wrapper (e.g. for images) in pixels. */
|
/** Maximum width of content wrapper (e.g. for images) in pixels. */
|
||||||
export const MAX_WIDTH = 865;
|
export const MAX_WIDTH = 896;
|
||||||
|
8
lib/helpers/classnames.ts
Normal file
8
lib/helpers/classnames.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import clsx, { type ClassValue } from "clsx";
|
||||||
|
|
||||||
|
const cn = (...inputs: ClassValue[]) => {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default cn;
|
14
package.json
14
package.json
@ -20,7 +20,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@date-fns/tz": "^1.2.0",
|
"@date-fns/tz": "^1.2.0",
|
||||||
"@date-fns/utc": "^2.1.0",
|
"@date-fns/utc": "^2.1.0",
|
||||||
"@emotion/hash": "^0.9.2",
|
|
||||||
"@giscus/react": "^3.1.0",
|
"@giscus/react": "^3.1.0",
|
||||||
"@mdx-js/loader": "^3.1.0",
|
"@mdx-js/loader": "^3.1.0",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
@ -41,7 +40,6 @@
|
|||||||
"html-entities": "^2.6.0",
|
"html-entities": "^2.6.0",
|
||||||
"lucide-react": "0.503.0",
|
"lucide-react": "0.503.0",
|
||||||
"next": "15.4.0-canary.5",
|
"next": "15.4.0-canary.5",
|
||||||
"polished": "^4.3.1",
|
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-activity-calendar": "^2.7.10",
|
"react-activity-calendar": "^2.7.10",
|
||||||
@ -69,6 +67,8 @@
|
|||||||
"resend": "^4.4.0",
|
"resend": "^4.4.0",
|
||||||
"server-only": "0.0.1",
|
"server-only": "0.0.1",
|
||||||
"shiki": "^3.3.0",
|
"shiki": "^3.3.0",
|
||||||
|
"tailwind-merge": "^3.2.0",
|
||||||
|
"tailwindcss": "^4.1.4",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"valibot": "^1.0.0"
|
"valibot": "^1.0.0"
|
||||||
},
|
},
|
||||||
@ -76,6 +76,7 @@
|
|||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.25.1",
|
"@eslint/js": "^9.25.1",
|
||||||
"@jakejarvis/eslint-config": "^4.0.7",
|
"@jakejarvis/eslint-config": "^4.0.7",
|
||||||
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/node": "^22.14.1",
|
"@types/node": "^22.14.1",
|
||||||
"@types/prop-types": "^15.7.14",
|
"@types/prop-types": "^15.7.14",
|
||||||
@ -97,12 +98,10 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^15.5.1",
|
"lint-staged": "^15.5.1",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"schema-dts": "^1.1.5",
|
"schema-dts": "^1.1.5",
|
||||||
"stylelint": "^16.19.0",
|
|
||||||
"stylelint-config-css-modules": "^4.4.0",
|
|
||||||
"stylelint-config-standard": "^38.0.0",
|
|
||||||
"stylelint-prettier": "^5.0.3",
|
|
||||||
"typescript": "5.8.3"
|
"typescript": "5.8.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
@ -119,9 +118,6 @@
|
|||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx,md,mdx}": [
|
"*.{js,jsx,ts,tsx,md,mdx}": [
|
||||||
"eslint"
|
"eslint"
|
||||||
],
|
|
||||||
"*.css": [
|
|
||||||
"stylelint"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1108
pnpm-lock.yaml
generated
1108
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
@ -23,6 +23,10 @@
|
|||||||
"groupName": "react",
|
"groupName": "react",
|
||||||
"rangeStrategy": "pin"
|
"rangeStrategy": "pin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"matchPackageNames": ["tailwindcss", "@tailwindcss/*", "tailwind-merge", "prettier-plugin-tailwindcss"],
|
||||||
|
"groupName": "tailwindcss"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matchPackageNames": ["@mdx-js/*", "remark-*", "rehype-*", "unified", "unist-*", "@types/mdx"],
|
"matchPackageNames": ["@mdx-js/*", "remark-*", "rehype-*", "unified", "unist-*", "@types/mdx"],
|
||||||
"groupName": "mdx"
|
"groupName": "mdx"
|
||||||
@ -32,8 +36,8 @@
|
|||||||
"groupName": "eslint"
|
"groupName": "eslint"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchPackageNames": ["stylelint", "stylelint-*"],
|
"matchPackageNames": ["prettier", "prettier-*", "!prettier-plugin-tailwindcss"],
|
||||||
"groupName": "stylelint"
|
"groupName": "prettier"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchPackageNames": ["typescript"],
|
"matchPackageNames": ["typescript"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user