1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-06-27 17:05:42 -04:00

Tailwind redesign (#2387)

This commit is contained in:
2025-05-02 22:04:26 -04:00
committed by GitHub
parent c4f67f170b
commit 5058382f71
162 changed files with 2739 additions and 3554 deletions

View File

@ -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"
]
}
},

1
.npmrc
View File

@ -1,3 +1,2 @@
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=*stylelint*

View File

@ -10,3 +10,4 @@ pnpm-lock.yaml
# other
public/
.devcontainer/devcontainer.json
.vscode/

View File

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

View File

@ -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
View 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
View 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"
}

View File

@ -6,7 +6,7 @@
[![GitHub repo size](https://img.shields.io/github/repo-size/jakejarvis/jarv.is?color=009cdf&label=repo%20size&logo=git&logoColor=white)](https://github.com/jakejarvis/jarv.is)
[![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fjarv.is%2Fapi%2Fhits&query=%24.total.hits&logo=googleanalytics&logoColor=white&label=hits&color=salmon&cacheSeconds=1800)](https://jarv.is/api/hits)
My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Vercel](https://vercel.com/), [Upstash Redis](https://upstash.com/), [Giscus](https://giscus.app/), [and more](https://jarv.is/humans.txt).
My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Tailwind CSS](https://github.com/user-attachments/assets/dfe99976-c73d-46f1-8a50-f26338463ad8), [Upstash](https://upstash.com/), [Giscus](https://giscus.app/), [and more](https://jarv.is/humans.txt).
## 🕹️ Getting Started

View File

@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { unstable_cache as cache } from "next/cache";
import redis from "../../../lib/redis";
import { kv } from "@vercel/kv";
// cache response from the db
const getData = cache(
@ -14,7 +14,7 @@ const getData = cache(
}>;
}> => {
// get all keys (aka slugs)
const slugs = await redis.scan(0, {
const slugs = await kv.scan(0, {
match: "hits:*",
type: "string",
// set an arbitrary yet generous upper limit, just in case...
@ -22,7 +22,7 @@ const getData = cache(
});
// get the value (number of hits) for each key (the slug of the page)
const values = await redis.mget<string[]>(...slugs[1]);
const values = await kv.mget<string[]>(...slugs[1]);
// pair the slugs with their hit values
const pages = slugs[1].map((slug, index) => ({

View File

@ -1,8 +1,8 @@
import { env } from "../../lib/env";
import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg";
import PageTitle from "../../components/PageTitle";
import Video from "../../components/Video";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import Video from "@/components/video";
import { createMetadata } from "@/lib/helpers/metadata";
import type { VideoObject } from "schema-dts";
import thumbnail from "./thumbnail.png";

View File

@ -1,6 +1,6 @@
import PageTitle from "../../components/PageTitle";
import Comments from "../../components/Comments";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import Comments from "@/components/comments";
import { createMetadata } from "@/lib/helpers/metadata";
export const metadata = createMetadata({
title: "CLI",

View File

@ -1,10 +1,10 @@
"use server";
import { env } from "../../lib/env";
import { env } from "@/lib/env";
import { headers } from "next/headers";
import * as v from "valibot";
import { Resend } from "resend";
import * as config from "../../lib/config";
import siteConfig from "@/lib/config/site";
const ContactSchema = v.object({
// TODO: replace duplicate error messages with v.message() when released. see:
@ -101,7 +101,7 @@ export const send = async (state: ContactState, payload: FormData): Promise<Cont
from: `${data.output.name} <${env.RESEND_FROM_EMAIL || "onboarding@resend.dev"}>`,
replyTo: `${data.output.name} <${data.output.email}>`,
to: [env.RESEND_TO_EMAIL],
subject: `[${config.siteName}] Contact Form Submission`,
subject: `[${siteConfig.name}] Contact Form Submission`,
text: data.output.message,
});

View File

@ -1,84 +0,0 @@
.input {
width: 100%;
padding: 0.8em;
margin: 0.6em 0;
border: 2px solid var(--colors-light);
border-radius: 0.6em;
color: var(--colors-text);
background-color: var(--colors-super-duper-light);
}
.input:focus {
outline: none;
border-color: var(--colors-link);
}
.input.textarea {
margin-bottom: 0;
line-height: 1.5;
min-height: calc(5 * 1.5rem + 1.5em); /* avoid layout shift when textarea-autosize loads */
resize: vertical;
}
.input.invalid {
border-color: var(--colors-error);
}
.errorMessage {
font-size: 0.9em;
color: var(--colors-error);
}
.actionRow {
display: flex;
align-items: center;
margin-top: 0.6em;
min-height: 3.75em;
}
.submitButton {
display: flex;
align-items: center;
height: 3.25em;
padding: 1em 1.25em;
margin-right: 1.5em;
border: 0;
border-radius: 0.6em;
cursor: pointer;
user-select: none;
font-weight: 500;
color: var(--colors-text);
background-color: var(--colors-kinda-light);
}
.submitButton:hover,
.submitButton:focus-visible {
color: var(--colors-super-duper-light);
background-color: var(--colors-link);
}
.submitIcon {
width: 1.3em;
height: 1.3em;
margin-right: 0.5em;
}
.result {
display: flex;
align-items: center;
font-weight: 600;
}
.result.success {
color: var(--colors-success);
}
.result.error {
color: var(--colors-error);
}
.resultIcon {
width: 1.3em;
height: 1.3em;
margin-right: 0.25em;
}

View File

@ -1,17 +1,17 @@
"use client";
import { env } from "../../lib/env";
import { env } from "@/lib/env";
import { useActionState, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import Turnstile from "react-turnstile";
import clsx from "clsx";
import { SendIcon, LoaderIcon, CheckIcon, XIcon } from "lucide-react";
import Link from "../../components/Link";
import { SendIcon, Loader2Icon, CheckIcon, XIcon } from "lucide-react";
import Link from "@/components/link";
import Input from "@/components/input";
import Textarea from "@/components/textarea";
import Button from "@/components/button";
import { cn } from "@/lib/utils";
import { send, type ContactState, type ContactInput } from "./action";
import styles from "./form.module.css";
const ContactForm = () => {
const [formState, formAction, pending] = useActionState<ContactState, FormData>(send, {
success: false,
@ -26,8 +26,9 @@ const ContactForm = () => {
});
return (
<form action={formAction}>
<input
<form action={formAction} className="my-6 space-y-4">
<div>
<Input
type="text"
name="name"
placeholder="Name"
@ -36,11 +37,15 @@ const ContactForm = () => {
setFormFields({ ...formFields, name: e.target.value });
}}
disabled={pending || formState.success}
className={clsx(styles.input, !pending && formState.errors?.name && styles.invalid)}
className={cn(!pending && formState.errors?.name && "border-destructive")}
/>
{!pending && formState.errors?.name && <span className={styles.errorMessage}>{formState.errors.name[0]}</span>}
{!pending && formState.errors?.name && (
<span className="text-destructive text-[0.8rem] font-semibold">{formState.errors.name[0]}</span>
)}
</div>
<input
<div>
<Input
type="email"
name="email"
placeholder="Email"
@ -50,88 +55,86 @@ const ContactForm = () => {
setFormFields({ ...formFields, email: e.target.value });
}}
disabled={pending || formState.success}
className={clsx(styles.input, !pending && formState.errors?.email && styles.invalid)}
className={cn(!pending && formState.errors?.email && "border-destructive")}
/>
{!pending && formState.errors?.email && <span className={styles.errorMessage}>{formState.errors.email[0]}</span>}
{!pending && formState.errors?.email && (
<span className="text-destructive text-[0.8rem] font-semibold">{formState.errors.email[0]}</span>
)}
</div>
<TextareaAutosize
<div>
<Textarea
name="message"
placeholder="Write something..."
minRows={5}
value={formFields.message}
onChange={(e) => {
setFormFields({ ...formFields, message: e.target.value });
}}
disabled={pending || formState.success}
className={clsx(styles.input, styles.textarea, !pending && formState.errors?.message && styles.invalid)}
className={cn("min-h-24", !pending && formState.errors?.message && "border-destructive")}
/>
{!pending && formState.errors?.message && (
<span className={styles.errorMessage}>{formState.errors.message[0]}</span>
<span className="text-destructive text-[0.8rem] font-semibold">{formState.errors.message[0]}</span>
)}
<div
style={{
fontSize: "0.825em",
lineHeight: 1.75,
}}
>
<div className="text-muted-foreground mt-2 text-[0.8rem] leading-relaxed">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
stroke="currentColor"
strokeWidth="0"
viewBox="0 0 24 24"
width="1.25em"
height="1.25em"
style={{
width: "1.25em",
height: "1.25em",
verticalAlign: "text-top",
marginRight: "0.25em",
}}
xmlns="http://www.w3.org/2000/svg"
className="mr-1 inline-block size-[16px] align-text-top"
>
<path d="M22.27 19.385H1.73A1.73 1.73 0 010 17.655V6.345a1.73 1.73 0 011.73-1.73h20.54A1.73 1.73 0 0124 6.345v11.308a1.73 1.73 0 01-1.73 1.731zM5.769 15.923v-4.5l2.308 2.885 2.307-2.885v4.5h2.308V8.078h-2.308l-2.307 2.885-2.308-2.885H3.46v7.847zM21.232 12h-2.309V8.077h-2.307V12h-2.308l3.461 4.039z" />
</svg>{" "}
Basic{" "}
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" style={{ fontWeight: 600 }}>
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" className="font-semibold">
Markdown syntax
</Link>{" "}
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
</Link>
](https://jarv.is), and <code>`code`</code>.
</div>
</div>
<div style={{ margin: "1em 0" }}>
<div>
<div className="my-4">
<Turnstile sitekey={env.NEXT_PUBLIC_TURNSTILE_SITE_KEY} fixedSize />
</div>
{!pending && formState.errors?.["cf-turnstile-response"] && (
<span className={styles.errorMessage}>{formState.errors["cf-turnstile-response"][0]}</span>
<span className="text-destructive text-[0.8rem] font-semibold">
{formState.errors["cf-turnstile-response"][0]}
</span>
)}
</div>
<div className={styles.actionRow}>
<div className="mt-[0.6em] flex min-h-[3.75em] items-center">
{!formState.success && (
<button type="submit" disabled={pending} className={styles.submitButton}>
<Button type="submit" disabled={pending}>
{pending ? (
<>
<LoaderIcon size="1.3em" className={styles.submitIcon} /> <span>Sending...</span>
<Loader2Icon className="animate-spin" /> Sending...
</>
) : (
<>
<SendIcon size="1.3em" className={styles.submitIcon} /> <span>Send</span>
<SendIcon /> Send
</>
)}
</button>
</Button>
)}
{!pending && formState.message && (
<div className={clsx(styles.result, formState.success ? styles.success : styles.error)}>
<div
className={cn("ml-4 text-[0.9rem] font-semibold", formState.success ? "text-success" : "text-destructive")}
>
{formState.success ? (
<CheckIcon size="1.3em" className={styles.resultIcon} />
<CheckIcon className="inline size-[16px]" />
) : (
<XIcon size="1.3em" className={styles.resultIcon} />
<XIcon className="inline size-[16px]" />
)}{" "}
{formState.message}
<span className="ml-[2px]">{formState.message}</span>
</div>
)}
</div>

View File

@ -1,7 +1,6 @@
import { LockIcon } from "lucide-react";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import Link from "@/components/link";
import { createMetadata } from "@/lib/helpers/metadata";
import ContactForm from "./form";
@ -13,40 +12,19 @@ export const metadata = createMetadata({
const Page = () => {
return (
<div
style={{
maxWidth: "600px",
margin: "0 auto",
}}
>
<div className="w-full md:mx-auto md:w-2/3">
<PageTitle canonical="/contact">Contact</PageTitle>
<p>
<p className="my-5 text-[0.925rem] leading-relaxed md:text-base">
Fill out this quick form and I&rsquo;ll get back to you as soon as I can! You can also{" "}
<Link href="mailto:jake@jarv.is">email me directly</Link> or send me a direct message on{" "}
<Link href="https://bsky.app/profile/jarv.is">🦋 Bluesky</Link> or{" "}
<Link href="https://fediverse.jarv.is/@jake">🦣 Mastodon</Link>.
</p>
<p>
<LockIcon
size="0.975em"
style={{
marginRight: "0.15em",
stroke: "var(--colors-warning)",
verticalAlign: "middle",
}}
/>{" "}
<p className="my-5 text-[0.925rem] leading-relaxed md:text-base">
You can grab my public key here:{" "}
<Link href="https://jrvs.io/pgp" title="My Public Key">
<code
style={{
fontSize: "0.925em",
letterSpacing: "0.075em",
wordSpacing: "-0.3em",
}}
>
<Link href="https://jrvs.io/pgp" className="font-mono text-sm tracking-wider [word-spacing:-0.3em]">
6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39
</code>
</Link>
.
</p>

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { buildFeed } from "../../lib/helpers/build-feed";
import { buildFeed } from "@/lib/helpers/build-feed";
export const dynamic = "force-static";

View File

@ -1,5 +1,5 @@
import { NextResponse } from "next/server";
import { buildFeed } from "../../lib/helpers/build-feed";
import { buildFeed } from "@/lib/helpers/build-feed";
export const dynamic = "force-static";

View File

@ -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,
});

View File

@ -1,69 +1,317 @@
/*!
* 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;
@custom-variant dark (&:where([data-theme=dark] *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.5 0.134 242.749);
--primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--warning: oklch(0.67 0.179 58.318);
--success: oklch(0.63 0.194 149.214);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
}
html {
line-height: 1.15;
tab-size: 4;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-text-size-adjust: 100%;
[data-theme="dark"] {
--background: oklch(0.205 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.81 0.105 251.813);
--primary-foreground: oklch(0.379 0.146 265.522);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--warning: oklch(0.8 0.184 86.047);
--success: oklch(0.79 0.209 151.711);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
}
body {
margin: 0;
font-family: var(--fonts-sans);
background-color: var(--colors-background-inner);
@theme inline {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--container-default: var(--container-4xl);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-warning: var(--warning);
--color-success: var(--success);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
code,
kbd,
samp,
pre {
font-size: 1em;
font-family: var(--fonts-mono);
font-variant-ligatures: none; /* i hate them. fwiw. */
@theme {
--animate-wave: wave 5s ease 1s infinite;
--animate-heartbeat: heartbeat 10s ease 7.5s infinite;
--animate-loading: animation: loading 1.5s infinite ease-in-out both;
@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);
}
}
@keyframes loading {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(0.6);
}
}
}
small {
font-size: 80%;
@layer base {
body {
@apply bg-background text-foreground;
}
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
@layer components {
.markdown {
@apply text-[0.925rem] leading-relaxed first:mt-0 last:mb-0 md:text-base;
sub {
bottom: -0.25em;
}
p {
@apply my-5;
}
strong {
@apply font-semibold;
}
h1,
h2,
h3,
h4 {
@apply scroll-m-4;
sup {
top: -0.5em;
}
a:has(.anchor-icon) {
@apply text-muted-foreground! hover:text-primary! ml-2 inline-block px-2 align-baseline [&_.anchor-icon]:inline-block;
}
+ * {
@apply mt-0;
}
}
h1 {
@apply mt-0 mb-3.5 text-3xl leading-snug font-extrabold md:text-4xl;
button,
input,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
strong {
@apply font-black;
}
}
h2 {
@apply mt-6 mb-4 text-xl leading-snug font-bold first:mt-0 md:text-2xl;
button,
[type="button"],
[type="reset"],
[type="submit"] {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-appearance: button;
strong {
@apply font-extrabold;
}
code {
@apply text-[0.875em];
}
}
h3 {
@apply mt-6 mb-2.5 text-lg leading-relaxed font-semibold md:text-xl;
strong {
@apply font-bold;
}
code {
@apply text-[0.9em];
}
}
h4 {
@apply mt-6 mb-2 leading-normal font-semibold;
strong {
@apply font-bold;
}
}
hr {
@apply border-border mx-auto my-6 w-11/12 border-t-2;
+ * {
@apply mt-0;
}
}
blockquote {
@apply border-border text-muted-foreground mt-6 border-l-4 pl-4 italic;
}
img {
@apply my-8 rounded-md;
+ em {
@apply text-muted-foreground -mt-4 block text-center text-[0.875em] leading-normal font-medium not-italic;
}
}
picture {
@apply my-8 block rounded-md;
> img {
@apply my-0;
}
}
video {
@apply my-8 rounded-md;
}
figure {
@apply my-8;
> * {
@apply my-0;
}
}
figcaption {
@apply text-muted-foreground mt-3.5 text-[0.875em] leading-snug;
}
ul,
ol {
@apply my-5 pl-7;
> li {
@apply pl-1.5;
&::marker {
@apply text-muted-foreground font-normal;
}
}
ul,
ol {
@apply my-1;
}
}
ul {
@apply list-disc;
}
ol {
@apply list-decimal;
&[type="A"] {
@apply list-[upper-alpha];
}
&[type="a"] {
@apply list-[lower-alpha];
}
&[type="A" s] {
@apply list-[upper-alpha];
}
&[type="a" s] {
@apply list-[lower-alpha];
}
&[type="I"] {
@apply list-[upper-roman];
}
&[type="i"] {
@apply list-[lower-roman];
}
&[type="I" s] {
@apply list-[upper-roman];
}
&[type="i" s] {
@apply list-[lower-roman];
}
&[type="1"] {
@apply list-decimal;
}
}
li {
@apply my-0.5;
}
dl {
@apply my-5;
}
dd {
@apply mt-2 pl-7;
}
dt {
@apply mt-5 font-semibold;
}
:not(pre) > code {
@apply bg-muted relative rounded-sm px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold;
}
}
}

View File

@ -1,9 +1,9 @@
import { env } from "../../lib/env";
import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Video from "../../components/Video";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import Link from "@/components/link";
import Video from "@/components/video";
import { createMetadata } from "@/lib/helpers/metadata";
import type { VideoObject } from "schema-dts";
import thumbnail from "./thumbnail.png";
@ -51,25 +51,17 @@ const Page = () => {
poster={thumbnail.src}
/>
<p
style={{
textAlign: "center",
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
color: "var(--colors-medium-light)",
}}
>
<p className="text-muted-foreground mx-4 mt-5 mb-0 text-center text-sm leading-relaxed">
Video is property of{" "}
<Link href="https://www.hillaryclinton.com/" style={{ fontWeight: 700 }}>
<Link href="https://www.hillaryclinton.com/" className="font-bold">
Hillary for America
</Link>
, the{" "}
<Link href="https://democrats.org/" style={{ fontWeight: 700 }}>
<Link href="https://democrats.org/" className="font-bold">
Democratic National Committee
</Link>
, and{" "}
<Link href="https://cnnpressroom.blogs.cnn.com/" style={{ fontWeight: 700 }}>
<Link href="https://cnnpressroom.blogs.cnn.com/" className="font-bold">
CNN / WarnerMedia
</Link>
. &copy; 2016.

View File

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

View File

@ -1,27 +1,28 @@
import { env } from "../lib/env";
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 { defaultMetadata } from "../lib/helpers/metadata";
import * as config from "../lib/config";
import { MAX_WIDTH } from "../lib/config/constants";
import { ThemeProvider, ThemeScript } from "@/components/ui/theme-context";
import Header from "@/components/ui/header";
import Footer from "@/components/ui/footer";
import { SkipNavLink, SkipNavTarget } from "@/components/ui/skip-nav";
import { cn } from "@/lib/utils";
import { defaultMetadata } from "@/lib/helpers/metadata";
import siteConfig from "@/lib/config/site";
import authorConfig from "@/lib/config/author";
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 (
<html lang={env.NEXT_PUBLIC_SITE_LOCALE} suppressHydrationWarning>
<html
lang={env.NEXT_PUBLIC_SITE_LOCALE}
className={cn(GeistSans.variable, GeistMono.variable)}
suppressHydrationWarning
>
<head>
<ThemeScript />
@ -30,19 +31,19 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
"@context": "https://schema.org",
"@type": "Person",
"@id": `${env.NEXT_PUBLIC_BASE_URL}/#person`,
name: config.authorName,
name: authorConfig.name,
url: env.NEXT_PUBLIC_BASE_URL,
image: [`${env.NEXT_PUBLIC_BASE_URL}/opengraph-image.jpg`],
sameAs: [
env.NEXT_PUBLIC_BASE_URL,
`https://${config.authorSocial?.mastodon}`,
`https://github.com/${config.authorSocial?.github}`,
`https://bsky.app/profile/${config.authorSocial?.bluesky}`,
`https://twitter.com/${config.authorSocial?.twitter}`,
`https://medium.com/@${config.authorSocial?.medium}`,
`https://www.linkedin.com/in/${config.authorSocial?.linkedin}/`,
`https://www.facebook.com/${config.authorSocial?.facebook}`,
`https://www.instagram.com/${config.authorSocial?.instagram}/`,
`https://${authorConfig.social?.mastodon}`,
`https://github.com/${authorConfig.social?.github}`,
`https://bsky.app/profile/${authorConfig.social?.bluesky}`,
`https://twitter.com/${authorConfig.social?.twitter}`,
`https://medium.com/@${authorConfig.social?.medium}`,
`https://www.linkedin.com/in/${authorConfig.social?.linkedin}/`,
`https://www.facebook.com/${authorConfig.social?.facebook}`,
`https://www.instagram.com/${authorConfig.social?.instagram}/`,
],
}}
/>
@ -52,34 +53,29 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
"@context": "https://schema.org",
"@type": "WebSite",
"@id": `${env.NEXT_PUBLIC_BASE_URL}/#website`,
name: config.siteName,
name: siteConfig.name,
url: env.NEXT_PUBLIC_BASE_URL,
author: config.authorName,
description: config.description,
author: authorConfig.name,
description: siteConfig.description,
inLanguage: env.NEXT_PUBLIC_SITE_LOCALE,
license: config.licenseUrl,
license: `https://spdx.org/licenses/${siteConfig.license}.html`,
}}
/>
</head>
<body
className={clsx(GeistSans.variable, GeistMono.variable)}
style={{ ["--max-width" as string]: `${MAX_WIDTH}px` }}
>
<body className="bg-background text-foreground font-sans antialiased">
<ThemeProvider>
<SkipNavLink />
<div className={styles.layout}>
<Header />
<div className="max-w-default mx-auto w-full px-5 pt-2 pb-6">
<Header className="mb-4 h-24 w-full md:h-18" />
<main className={styles.default}>
<div className={styles.container}>
<main>
<SkipNavTarget />
{children}
</div>
</main>
<Footer />
<Footer className="mt-6 w-full" />
</div>
</ThemeProvider>

View File

@ -1,9 +1,9 @@
import { env } from "../../lib/env";
import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import Video from "../../components/Video";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import Link from "@/components/link";
import Video from "@/components/video";
import { createMetadata } from "@/lib/helpers/metadata";
import type { VideoObject } from "schema-dts";
import thumbnail from "./thumbnail.png";
@ -50,21 +50,13 @@ const Page = () => {
poster={thumbnail.src}
/>
<p
style={{
textAlign: "center",
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
color: "var(--colors-medium-light)",
}}
>
<p className="text-muted-foreground mx-4 mt-5 mb-0 text-center text-sm leading-relaxed">
Video is property of{" "}
<Link href="https://web.archive.org/web/20070511004304/www.g4techtv.ca" style={{ fontWeight: 700 }}>
<Link href="https://web.archive.org/web/20070511004304/www.g4techtv.ca" className="font-bold">
G4techTV Canada
</Link>{" "}
&amp;{" "}
<Link href="https://leo.fm/" style={{ fontWeight: 700 }}>
<Link href="https://leo.fm/" className="font-bold">
Leo Laporte
</Link>
. &copy; 2007 G4 Media, Inc.

View File

@ -1,5 +1,5 @@
import PageTitle from "../../components/PageTitle";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import { createMetadata } from "@/lib/helpers/metadata";
export const metadata = createMetadata({
title: "License",

View File

@ -1,13 +1,13 @@
import { env } from "../lib/env";
import * as config from "../lib/config";
import { env } from "@/lib/env";
import siteConfig from "@/lib/config/site";
import type { MetadataRoute } from "next";
const manifest = (): MetadataRoute.Manifest => {
return {
name: config.siteName,
name: siteConfig.name,
// eslint-disable-next-line camelcase
short_name: config.siteName,
description: config.description,
short_name: siteConfig.name,
description: siteConfig.description,
lang: env.NEXT_PUBLIC_SITE_LOCALE,
icons: [
{

View File

@ -1,5 +1,5 @@
import Video from "../components/Video";
import Link from "../components/Link";
import Video from "@/components/video";
import Link from "@/components/link";
import type { Metadata } from "next";
export const metadata: Metadata = {
@ -17,15 +17,15 @@ const Page = () => {
<Video
src="https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/not-found-SAtLyNyc7gVhveYxr6o1ITd9CSXo5X.mp4"
autoPlay
style={{ maxWidth: 480, aspectRatio: "16/11" }}
className="mt-6 aspect-[16/11] max-w-[480px] rounded-lg"
/>
<div style={{ textAlign: "center", marginTop: "1.5em" }}>
<h1 style={{ margin: "0.5em 0", fontSize: "2.2em", fontWeight: 500, lineHeight: 1 }}>Page Not Found</h1>
<div className="mt-6 text-center">
<h1 className="my-2 text-2xl font-semibold md:text-3xl">Page Not Found</h1>
<Link href="/" style={{ fontSize: "1.2em", fontWeight: 500 }}>
Go home?
</Link>
<p className="mt-4 mb-0 text-lg font-medium md:text-xl">
<Link href="/">Go home?</Link>
</p>
</div>
</>
);

View File

@ -1,11 +1,11 @@
import { env } from "../../../lib/env";
import { ImageResponse } from "next/og";
import { notFound } from "next/navigation";
import { env } from "@/lib/env";
import path from "path";
import fs from "fs";
import { getSlugs, getFrontMatter } from "../../../lib/helpers/posts";
import * as config from "../../../lib/config";
import { POSTS_DIR } from "../../../lib/config/constants";
import { ImageResponse } from "next/og";
import { notFound } from "next/navigation";
import { getSlugs, getFrontMatter } from "@/lib/helpers/posts";
import siteConfig from "@/lib/config/site";
import { POSTS_DIR } from "@/lib/config/constants";
export const contentType = "image/png";
export const size = {
@ -150,7 +150,7 @@ const OpenGraphImage = async ({ params }: { params: Promise<{ slug: string }> })
marginLeft: "0.75rem",
}}
>
{config.siteName}
{siteConfig.name}
</span>
</div>

View File

@ -1,77 +0,0 @@
.meta {
display: flex;
justify-items: flex-start;
flex-wrap: wrap;
font-size: 0.925em;
line-height: 2.3;
letter-spacing: 0.04em;
color: var(--colors-medium);
}
.meta .metaItem {
display: inline-flex;
align-items: center;
margin-right: 1.6em;
white-space: nowrap;
}
.meta .metaLink {
color: inherit;
}
.meta .metaIcon {
width: 1.25em;
height: 1.25em;
margin-right: 0.6em;
flex-shrink: 0;
}
.meta .metaTags {
display: inline-flex;
flex-wrap: wrap;
white-space: normal;
}
.meta .metaTag {
text-transform: lowercase;
white-space: nowrap;
margin-right: 0.75em;
}
.meta .metaTag::before {
content: "\0023"; /* cosmetically hashtagify tags */
padding-right: 0.125em;
color: var(--colors-light);
}
.meta .metaTag:last-of-type {
margin-right: 0;
}
.title {
margin: 0.3em 0 0.5em -1px; /* misaligned left margin, super nitpicky */
font-size: 2.3em;
line-height: 1.3;
font-weight: 700;
}
.title code {
margin: 0 0.075em;
}
.title .link {
color: var(--colors-text);
}
.comments {
margin-top: 2em;
padding-top: 2em;
border-top: 2px solid var(--colors-light);
min-height: 140px;
}
@media (max-width: 768px) {
.title {
font-size: 1.9em;
}
}

View File

@ -1,23 +1,21 @@
import { env } from "../../../lib/env";
import { env } from "@/lib/env";
import { Suspense } from "react";
import { JsonLd } from "react-schemaorg";
import clsx from "clsx";
import { CalendarDaysIcon, TagIcon, SquarePenIcon, EyeIcon } from "lucide-react";
import Link from "../../../components/Link";
import Time from "../../../components/Time";
import Comments from "../../../components/Comments";
import Loading from "../../../components/Loading";
import HitCounter from "./counter";
import { getSlugs, getFrontMatter } from "../../../lib/helpers/posts";
import { createMetadata } from "../../../lib/helpers/metadata";
import * as config from "../../../lib/config";
import { POSTS_DIR } from "../../../lib/config/constants";
import Link from "@/components/link";
import Time from "@/components/time";
import Comments from "@/components/comments";
import Loading from "@/components/loading";
import ViewCounter from "@/components/view-counter";
import { getSlugs, getFrontMatter } from "@/lib/helpers/posts";
import { createMetadata } from "@/lib/helpers/metadata";
import siteConfig from "@/lib/config/site";
import authorConfig from "@/lib/config/author";
import { POSTS_DIR } from "@/lib/config/constants";
import { size as ogImageSize } from "./opengraph-image";
import type { Metadata } from "next";
import type { BlogPosting } from "schema-dts";
import styles from "./page.module.css";
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params#disable-rendering-for-unspecified-paths
export const dynamicParams = false;
@ -43,7 +41,7 @@ export const generateMetadata = async ({ params }: { params: Promise<{ slug: str
canonical: `/${POSTS_DIR}/${slug}`,
openGraph: {
type: "article",
authors: [config.authorName],
authors: [authorConfig.name],
tags: frontmatter!.tags,
publishedTime: frontmatter!.date,
modifiedTime: frontmatter!.date,
@ -61,7 +59,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
const { default: MDXContent } = await import(`../../../${POSTS_DIR}/${slug}/index.mdx`);
return (
<article>
<>
<JsonLd<BlogPosting>
item={{
"@context": "https://schema.org",
@ -79,7 +77,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
datePublished: frontmatter!.date,
dateModified: frontmatter!.date,
inLanguage: env.NEXT_PUBLIC_SITE_LOCALE,
license: config.licenseUrl,
license: `https://spdx.org/licenses/${siteConfig.license}.html`,
author: {
// defined in app/layout.tsx
"@id": `${env.NEXT_PUBLIC_BASE_URL}/#person`,
@ -87,73 +85,67 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
}}
/>
<div className={styles.meta}>
<Link href={`/${POSTS_DIR}/${frontmatter!.slug}`} plain className={clsx(styles.metaItem, styles.metaLink)}>
<CalendarDaysIcon size="1.25em" className={styles.metaIcon} />
<div className="text-foreground/70 -mt-1 flex flex-wrap justify-items-start text-[0.8rem] leading-9 tracking-wide md:text-[0.85rem]">
<Link href={`/${POSTS_DIR}/${frontmatter!.slug}`} className={"text-foreground/70 mr-4 whitespace-nowrap"}>
<CalendarDaysIcon className="mr-2 inline size-[16px] shrink-0 align-text-bottom" />
<Time date={frontmatter!.date} format="MMMM d, y" />
</Link>
{frontmatter!.tags && (
<div className={styles.metaItem}>
<TagIcon size="1.25em" className={styles.metaIcon} />
<span className={styles.metaTags}>
<div className="mr-4">
<TagIcon className="mr-2 inline size-[16px] shrink-0 align-text-bottom" />
{frontmatter!.tags.map((tag) => (
<span key={tag} title={tag} className={styles.metaTag} aria-label={`Tagged with ${tag}`}>
<span
key={tag}
title={tag}
className="before:text-foreground/40 mr-2 lowercase before:pr-0.5 before:content-['\0023'] last-of-type:mr-0"
aria-label={`Tagged with ${tag}`}
>
{tag}
</span>
))}
</span>
</div>
)}
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter!.slug}/index.mdx`}
title={`Edit "${frontmatter!.title}" on GitHub`}
plain
className={clsx(styles.metaItem, styles.metaLink)}
className={"text-foreground/70 mr-4 whitespace-nowrap"}
>
<SquarePenIcon size="1.25em" className={styles.metaIcon} />
<SquarePenIcon className="mr-2 inline size-[16px] shrink-0 align-text-bottom" />
<span>Improve This Post</span>
</Link>
<div
className={styles.metaItem}
style={{
// fix potential layout shift when number of hits loads
minWidth: "6em",
marginRight: 0,
}}
>
<EyeIcon size="1.25em" className={styles.metaIcon} />
<div className="mr-0 min-w-10 whitespace-nowrap">
<EyeIcon className="mr-2 inline size-[16px] shrink-0 align-text-bottom" />
<Suspense
// when this loads, the component will count up from zero to the actual number of hits, so we can simply
// show a zero here as a "loading indicator"
fallback={<span>0</span>}
>
<HitCounter slug={`${POSTS_DIR}/${frontmatter!.slug}`} />
<ViewCounter slug={`${POSTS_DIR}/${frontmatter!.slug}`} />
</Suspense>
</div>
</div>
<h1 className={styles.title}>
<h1 className="mt-2 mb-3 text-3xl/10 font-bold md:text-4xl/12">
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
plain
className={styles.link}
className="text-foreground hover:no-underline"
/>
</h1>
<MDXContent />
{!frontmatter!.noComments && (
<div id="comments" className={styles.comments}>
<Suspense fallback={<Loading boxes={3} width={40} style={{ display: "block", margin: "2em auto" }} />}>
<div id="comments" className="border-ring mt-8 min-h-36 border-t-2 pt-8">
<Suspense fallback={<Loading boxes={3} width={40} className="mx-auto my-8 block" />}>
<Comments title={frontmatter!.title} />
</Suspense>
</div>
)}
</article>
</>
);
};

View File

@ -1,52 +0,0 @@
.section {
font-size: 1.1em;
line-height: 1.1;
margin: 2.4em 0;
}
.section:first-of-type {
margin-top: 0.25em;
}
.section:last-of-type {
margin-bottom: 0.25em;
}
.year {
font-size: 2.2em;
font-weight: 700;
margin-top: 0;
margin-bottom: 0.5em;
}
.list {
list-style-type: none;
margin: 0;
padding: 0;
}
.post {
display: flex;
line-height: 1.75;
margin-bottom: 1em;
}
.post:last-of-type {
margin-bottom: 0;
}
.date {
width: 5.25em;
flex-shrink: 0;
color: var(--colors-medium);
}
@media (max-width: 768px) {
.section {
margin: 1.8em 0;
}
.year {
font-size: 2em;
}
}

View File

@ -1,17 +1,15 @@
import Link from "../../components/Link";
import Time from "../../components/Time";
import { getFrontMatter } from "../../lib/helpers/posts";
import { createMetadata } from "../../lib/helpers/metadata";
import * as config from "../../lib/config";
import { POSTS_DIR } from "../../lib/config/constants";
import Link from "@/components/link";
import Time from "@/components/time";
import { getFrontMatter } from "@/lib/helpers/posts";
import { createMetadata } from "@/lib/helpers/metadata";
import authorConfig from "@/lib/config/author";
import { POSTS_DIR } from "@/lib/config/constants";
import type { ReactElement } from "react";
import type { FrontMatter } from "../../lib/helpers/posts";
import styles from "./page.module.css";
import type { FrontMatter } from "@/lib/helpers/posts";
export const metadata = createMetadata({
title: "Notes",
description: `Recent posts by ${config.authorName}.`,
description: `Recent posts by ${authorConfig.name}.`,
canonical: `/${POSTS_DIR}`,
});
@ -31,12 +29,12 @@ const Page = async () => {
Object.entries(postsByYear).forEach(([year, posts]) => {
sections.push(
<section className={styles.section} key={year}>
<h2 className={styles.year}>{year}</h2>
<ul className={styles.list}>
<section className="my-8 first-of-type:mt-0" key={year}>
<h2 className="mt-0 mb-4 text-3xl font-bold md:text-4xl">{year}</h2>
<ul>
{posts.map(({ slug, date, title, htmlTitle }) => (
<li className={styles.post} key={slug}>
<Time date={date} format="MMM d" className={styles.date} />
<li className="mb-4 flex text-base leading-relaxed last-of-type:mb-0" key={slug}>
<Time date={date} format="MMM d" className="text-muted-foreground w-22 shrink-0" />
<span>
<Link
dynamicOnHover
@ -54,7 +52,7 @@ const Page = async () => {
// grouped posts enter this component ordered chronologically -- we want reverse chronological
const reversed = sections.reverse();
return reversed;
return <>{reversed}</>;
};
export default Page;

View File

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

View File

@ -1,105 +1,51 @@
import clsx from "clsx";
import hash from "@emotion/hash";
import { rgba } from "polished";
import Link from "@/components/link";
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 = () => {
return (
<div className={styles.index}>
<h1>
Hi there! I&rsquo;m Jake. <span className={styles.wave}>👋</span>
<>
<h1 className="mt-0 mb-2 text-3xl leading-relaxed font-medium">
Hi there! I&rsquo;m Jake. <span className="animate-wave ml-0.5 inline-block origin-[65%_80%] text-3xl">👋</span>
</h1>
<h2>
<h2 className="my-2 text-xl leading-relaxed font-normal">
I&rsquo;m a frontend web developer based in the{" "}
<Link
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&amp;fs=1&amp;showinfo=1&amp;rel=0&amp;iv_load_policy=3"
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
lightColor="#fb4d42"
darkColor="#ff5146"
className="[--primary:#fb4d42] dark:[--primary:#ff5146]"
>
Boston
</Link>{" "}
area.
</h2>
<p>
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
I specialize in using{" "}
<Link
href="https://www.typescriptlang.org/"
title="TypeScript Official Website"
lightColor="#235a97"
darkColor="#59a8ff"
>
<Link href="https://www.typescriptlang.org/" className="[--primary:#235a97] dark:[--primary:#59a8ff]">
TypeScript
</Link>
,{" "}
<Link href="https://reactjs.org/" title="React Official Website" lightColor="#1091b3" darkColor="#6fcbe3">
<Link href="https://reactjs.org/" className="[--primary:#1091b3] dark:[--primary:#6fcbe3]">
React
</Link>
, and{" "}
<Link href="https://nextjs.org/" title="Next.js Official Website" lightColor="#5e7693" darkColor="#a8b9c0">
<Link href="https://nextjs.org/" className="[--primary:#5e7693] dark:[--primary:#a8b9c0]">
Next.js
</Link>{" "}
to make lightweight{" "}
<Link
href="https://jamstack.org/glossary/jamstack/"
title="Jamstack Glossary"
lightColor="#04a699"
darkColor="#08bbac"
>
<Link href="https://jamstack.org/glossary/jamstack/" className="[--primary:#04a699] dark:[--primary:#08bbac]">
Jamstack sites
</Link>{" "}
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="[--primary:#6fbc4e] dark:[--primary:#84d95f]">
Node
</Link>{" "}
backends. But I still know my way around{" "}
<Link
href="https://www.jetbrains.com/lp/php-25/"
title="25 Years of PHP History"
lightColor="#8892bf"
darkColor="#a4afe3"
className="[--primary:#8892bf] dark:[--primary:#a4afe3]"
>
less buzzwordy
</Link>{" "}
@ -107,21 +53,19 @@ const Page = () => {
<Link
href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/"
title='"The Cost of Javascript Frameworks" by Tim Kadlec'
lightColor="#f48024"
darkColor="#e18431"
className="[--primary:#f48024] dark:[--primary:#e18431]"
>
vanilla JavaScript
</Link>
), too.
</p>
<p>
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
Whenever possible, I also apply my experience in{" "}
<Link
href="https://bugcrowd.com/jakejarvis"
title="Jake Jarvis on Bugcrowd"
lightColor="#00b81a"
darkColor="#57f06d"
className="[--primary:#00b81a] dark:[--primary:#57f06d]"
>
application security
</Link>
@ -129,8 +73,7 @@ const Page = () => {
<Link
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
title='"What is serverless computing?" on Cloudflare'
lightColor="#0098ec"
darkColor="#43b9fb"
className="[--primary:#0098ec] dark:[--primary:#43b9fb]"
>
serverless stacks
</Link>
@ -138,21 +81,19 @@ const Page = () => {
<Link
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=source&language=&sort=stargazers"
title='My repositories tagged with "github-actions" on GitHub'
lightColor="#ff6200"
darkColor="#f46c16"
className="[--primary:#ff6200] dark:[--primary:#f46c16]"
>
DevOps automation
automation
</Link>
.
</p>
<p>
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
I fell in love with{" "}
<Link
href="/previously"
title="My Terrible, Horrible, No Good, Very Bad First Websites"
lightColor="#4169e1"
darkColor="#8ca9ff"
className="[--primary:#4169e1] dark:[--primary:#8ca9ff]"
>
frontend web design
</Link>{" "}
@ -160,8 +101,7 @@ const Page = () => {
<Link
href="/notes/my-first-code"
title="Jake's Bulletin Board, circa 2003"
lightColor="#9932cc"
darkColor="#d588fb"
className="[--primary:#9932cc] dark:[--primary:#d588fb]"
>
backend programming
</Link>{" "}
@ -169,147 +109,109 @@ const Page = () => {
<Link
href="/birthday"
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
lightColor="#e40088"
darkColor="#fd40b1"
className="[--primary:#e40088] dark:[--primary:#fd40b1]"
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`,
}}
>
the Tooth Fairy
</Link>
. <span style={{ color: "var(--colors-medium-light)" }}>I&rsquo;ve improved a bit since then, I think? 🤷</span>
. <span className="text-muted-foreground">I&rsquo;ve improved a bit since then, I think? 🤷</span>
</p>
<p>
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
Over the years, some of my side projects{" "}
<Link
href="/leo"
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
lightColor="#ff1b1b"
darkColor="#f06060"
className="[--primary:#ff1b1b] dark:[--primary:#f06060]"
>
have
</Link>{" "}
<Link
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
lightColor="#f78200"
darkColor="#fd992a"
className="[--primary:#f78200] dark:[--primary:#fd992a]"
>
been
</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"
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
lightColor="#f2b702"
darkColor="#ffcc2e"
className="[--primary:#f2b702] dark:[--primary:#ffcc2e]"
>
featured
</Link>{" "}
<Link
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
title='"The new Facebook is on a roll" on CNN Money'
lightColor="#5ebd3e"
darkColor="#78df55"
className="[--primary:#5ebd3e] dark:[--primary:#78df55]"
>
by
</Link>{" "}
<Link
href="https://www.wired.com/2007/04/our-web-servers/"
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
lightColor="#009cdf"
darkColor="#29bfff"
className="[--primary:#009cdf] dark:[--primary:#29bfff]"
>
various
</Link>{" "}
<Link
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
lightColor="#3e49bb"
darkColor="#7b87ff"
className="[--primary:#3e49bb] dark:[--primary:#7b87ff]"
>
media
</Link>{" "}
<Link
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
title='"Your Next Client? The CEO&#39;s Son" on Advertising Age'
lightColor="#973999"
darkColor="#db60dd"
className="[--primary:#973999] dark:[--primary:#db60dd]"
>
outlets
</Link>
.
</p>
<p>
<p className="mt-3 mb-0 text-base leading-relaxed md:text-[0.975rem]">
You can find my work on{" "}
<Link
href="https://github.com/jakejarvis"
rel="me"
title="Jake Jarvis on GitHub"
lightColor="#8d4eff"
darkColor="#a379f0"
>
<Link href="https://github.com/jakejarvis" rel="me" className="[--primary:#8d4eff] dark:[--primary:#a379f0]">
GitHub
</Link>{" "}
and{" "}
<Link
href="https://www.linkedin.com/in/jakejarvis/"
rel="me"
title="Jake Jarvis on LinkedIn"
lightColor="#0073b1"
darkColor="#3b9dd2"
className="[--primary:#0073b1] dark:[--primary:#3b9dd2]"
>
LinkedIn
</Link>
. I&rsquo;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="[--primary:#de0c0c] dark:[--primary:#ff5050]">
email
</Link>{" "}
<sup>
<sup className="mx-0.5 text-[0.6rem]">
<Link
href="https://jrvs.io/pgp"
rel="pgpkey"
title="My Public Key"
lightColor="#757575"
darkColor="#959595"
plain
className="text-muted-foreground hover:decoration-muted-foreground/40 text-nowrap hover:decoration-1"
>
<LockIcon size="1.25em" style={{ verticalAlign: "text-top" }} />{" "}
<code
style={{
margin: "0 0.15em",
letterSpacing: "0.075em",
wordSpacing: "-0.4em",
}}
>
2B0C 9CF2 51E6 9A39
</code>
<LockIcon className="mr-[1px] inline size-3 align-text-top" />
<code className="mx-0.5 tracking-wider text-wrap [word-spacing:-4px]">2B0C 9CF2 51E6 9A39</code>
</Link>
</sup>
,{" "}
<Link
href="https://bsky.app/profile/jarv.is"
rel="me"
title="Jake Jarvis on Bluesky"
lightColor="#0085ff"
darkColor="#208bfe"
>
<Link href="https://bsky.app/profile/jarv.is" rel="me" className="[--primary:#0085ff] dark:[--primary:#208bfe]">
Bluesky
</Link>
, or{" "}
<Link
href="https://fediverse.jarv.is/@jake"
rel="me"
title="Jake Jarvis on Mastodon"
lightColor="#6d6eff"
darkColor="#7b87ff"
>
<Link href="https://fediverse.jarv.is/@jake" rel="me" className="[--primary:#6d6eff] dark:[--primary:#7b87ff]">
Mastodon
</Link>{" "}
as well!
</p>
</div>
</>
);
};

View File

@ -1,6 +1,6 @@
import { Comic_Neue as ComicNeueLoader } from "next/font/google";
import PageTitle from "../../components/PageTitle";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import { createMetadata } from "@/lib/helpers/metadata";
export const metadata = createMetadata({
title: "Previously on...",
@ -15,6 +15,7 @@ export const ComicNeue = ComicNeueLoader({
display: "swap",
fallback: ["'Comic Sans MS'", "'Comic Sans'"],
adjustFontFallback: false,
variable: "--font-comic-neue",
preload: false,
});
@ -30,19 +31,18 @@ a, button {
cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAgMAAAAOFJJnAAAACVBMVEVHcEwAAAD///8W1S+BAAAAAXRSTlMAQObYZgAAAEdJREFUeAFjoAVghTGkHIhghMAYmQEwxlIYYxlYlSiQMQEsELUKyli1ahWYwQZjMGIwGLKQGA4QA1EYEP0rGVAZrKGhSF4BAHw/HsVwshytAAAAAElFTkSuQmCC") 16 12, auto;
}
main {
font-family: ${ComicNeue.style.fontFamily}, var(--fonts-sans) !important;
font-family: ${ComicNeue.style.fontFamily}, var(--default-font-family) !important;
font-weight: 700 !important;
font-size: 1em !important;
text-align: center;
}
main iframe + p em {
main iframe + p em,
main img + em {
display: block;
text-align: center;
margin-top: -0.4em;
font-style: normal;
font-size: 0.95em;
line-height: 1.5;
color: var(--colors-medium);
font-weight: 700;
}
`.trim(),
}}
@ -56,14 +56,7 @@ export const WindowsLogo = () => (
stroke="currentColor"
strokeWidth="0"
viewBox="0 0 24 24"
width="1.25em"
height="1.25em"
style={{
width: "1.25em",
height: "1.25em",
verticalAlign: "text-bottom",
marginRight: "0.1em",
}}
className="inline size-[16px] align-text-top"
>
<path d="M5.712 1.596l-.756.068-.238.55.734-.017zm1.39.927l-.978.137-.326.807.96-.12.345-.824zM4.89 3.535l-.72.05-.24.567.721-.017zm3.724.309l-1.287.068-.394.96 1.27-.052zm1.87.566l-1.579.069-.566 1.357 1.596-.088.548-1.338zm-4.188.037l-.977.153-.343.806.976-.12zm6.144.668l-1.87.135-.637 1.527 1.87-.154zm2.925.219c-.11 0-.222 0-.334.002l-.767 1.85c1.394-.03 2.52.089 3.373.38l-1.748 4.201c-.955-.304-2.082-.444-3.36-.394l-.54 1.305a8.762 8.762 0 0 1 3.365.396l-1.663 4.014c-1.257-.27-2.382-.395-3.387-.344l-.782 1.887c3.363-.446 6.348.822 9.009 3.773L24 9.23c-2.325-2.575-5.2-3.88-8.637-3.896zm-.644.002l-2.024.12-.687 1.68 2.025-.19zm-10.603.05l-.719.036-.224.566h.703l.24-.601zm3.69.397l-1.287.069-.395.959 1.27-.05zM5.54 6.3l-.994.154-.344.807.98-.121zm4.137.066l-1.58.069L7.53 7.77l1.596-.085.55-1.32zm1.955.688l-1.87.135-.636 1.527 1.887-.154zm2.282.19l-2.01.136-.7 1.682 2.04-.19.67-1.63zm-10.57.066l-.739.035-.238.564h.72l.257-.6zm3.705.293l-1.303.085-.394.96 1.287-.034zm11.839.255a6.718 6.718 0 0 1 2.777 1.717l-1.75 4.237c-.617-.584-1.15-.961-1.611-1.149l-1.201-.498zM4.733 8.22l-.976.154-.344.807.961-.12.36-.841zm4.186 0l-1.594.052-.549 1.354L8.37 9.54zm1.957.668L8.99 9.04l-.619 1.508 1.87-.135.636-1.527zm2.247.275l-2.007.12-.703 1.665 2.042-.156zM2.52 9.267l-.718.033-.24.549.718-.016zm3.725.273l-1.289.07-.41.96 1.287-.03.412-1zm1.87.6l-1.596.05-.55 1.356 1.598-.084.547-1.322zm-4.186.037l-.979.136-.324.805.96-.119zm6.14.633l-1.87.154-.653 1.527 1.906-.154zm2.267.275l-2.026.12-.686 1.663 2.025-.172zm-10.569.031l-.739.037-.238.565.72-.016zm3.673.362l-1.289.068-.41.978 1.305-.05zm-2.285.533l-.976.154-.326.805.96-.12.342-.84zm4.153.07l-1.596.066-.565 1.356 1.612-.084zm1.957.666l-1.889.154-.617 1.526 1.886-.15zm2.28.223l-2.025.12-.685 1.665 2.041-.172.67-1.613zm-10.584.05l-.738.053L0 13.64l.72-.02.24-.6zm3.705.31l-1.285.07-.395.976 1.287-.05.393-.997zm11.923.07c1.08.29 2.024.821 2.814 1.613l-1.715 4.183c-.892-.754-1.82-1.32-2.814-1.664l1.715-4.133zm-10.036.515L4.956 14l-.549 1.32 1.578-.066.567-1.338zm-4.184.014l-.996.156-.309.79.961-.106zm6.14.67l-1.904.154-.617 1.527 1.89-.154.632-1.527zm2.231.324l-2.025.123-.686 1.682 2.026-.174zm-6.863.328l-1.3.068-.397.98 1.285-.054zm1.871.584l-1.578.068-.566 1.334 1.595-.064zm1.953.701l-1.867.137-.635 1.51 1.87-.137zm2.23.31l-2.005.122-.703 1.68 2.04-.19.67-1.61z" />
</svg>
@ -71,7 +64,9 @@ export const WindowsLogo = () => (
<PageStyles />
<PageTitle canonical="/previously">Previously</PageTitle>
<PageTitle canonical="/previously" className="font-semibold">
<>Previously</>
</PageTitle>
![Timeline of this website's past.](./images/wayback.png)
_Previously on the [Cringey Chronicles&trade;](https://web.archive.org/web/20010501000000*/jakejarvis.com) of this website's past..._
@ -85,7 +80,8 @@ _Previously on the [Cringey Chronicles&trade;](https://web.archive.org/web/20010
<iframe
src="https://jakejarvis.github.io/my-first-website/"
title="My Terrible, Horrible, No Good, Very Bad First Website"
style={{ height: "500px", width: "100%", border: "1px solid var(--colors-kinda-light)", marginBottom: "-0.4em" }}
className="border-ring w-full border-2"
style={{ height: "500px" }}
/>
_[November 2001](https://jakejarvis.github.io/my-first-website/) ([view
source](https://github.com/jakejarvis/my-first-website))_

View File

@ -1,5 +1,5 @@
import PageTitle from "../../components/PageTitle";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import { createMetadata } from "@/lib/helpers/metadata";
export const metadata = createMetadata({
title: "Privacy",

View File

@ -31,7 +31,7 @@ const Calendar = ({ data, ...rest }: CalendarProps) => {
],
}}
labels={{
totalCount: "{{count}} contributions in the last year",
totalCount: `{{count}} contributions in the last year`,
}}
maxLevel={4}
renderBlock={(block, activity) =>

View File

@ -1,7 +1,7 @@
// https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment
import "server-only";
import { env } from "../../lib/env";
import { env } from "@/lib/env";
import * as cheerio from "cheerio";
import { graphql } from "@octokit/graphql";
import type { Repository, User } from "@octokit/graphql-schema";

View File

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

View File

@ -1,16 +1,15 @@
import { env } from "../../lib/env";
import { env } from "@/lib/env";
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { GitForkIcon, StarIcon } from "lucide-react";
import Calendar from "./calendar";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import RelativeTime from "../../components/RelativeTime";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import Link from "@/components/link";
import RelativeTime from "@/components/relative-time";
import ActivityCalendar from "./components/activity-calendar";
import { cn } from "@/lib/utils";
import { createMetadata } from "@/lib/helpers/metadata";
import { getContributions, getRepos } from "./github";
import styles from "./page.module.css";
export const metadata = createMetadata({
title: "Projects",
description: `Most-starred repositories by @${env.NEXT_PUBLIC_GITHUB_USERNAME} on GitHub`,
@ -32,41 +31,52 @@ const Page = async () => {
<>
<PageTitle canonical="/projects">Projects</PageTitle>
<h2 className={styles.heading}>
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`} style={{ color: "inherit" }} plain>
<h2 className="my-3.5 text-xl font-medium">
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}
className="text-secondary-foreground hover:no-underline"
>
Contribution activity
</Link>
</h2>
<Suspense fallback={<p>Failed to generate activity calendar.</p>}>
<Calendar data={contributions} className={styles.calendar} />
<div
className={cn(
"mx-auto mt-4 mb-8",
String.raw`[&_:where(.react-activity-calendar\_\_count,.react-activity-calendar\_\_legend-month,.react-activity-calendar\_\_legend-colors)]:text-muted-foreground`,
"[--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]"
)}
>
<ActivityCalendar data={contributions} />
</div>
</Suspense>
<h2 className={styles.heading}>
<h2 className="my-3.5 text-xl font-medium">
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`}
style={{ color: "inherit" }}
plain
className="text-secondary-foreground hover:no-underline"
>
Popular repositories
</Link>
</h2>
<div className={styles.grid}>
<div className="row-auto grid w-full grid-cols-none gap-4 lg:grid-cols-2">
{repos?.map((repo) => (
<div key={repo!.name} className={styles.card}>
<Link href={repo!.url} className={styles.name}>
<div key={repo!.name} className="border-ring/65 h-fit rounded-2xl border-1 p-4">
<Link href={repo!.url} className="mb-2 inline-block text-base font-semibold">
{repo!.name}
</Link>
{repo!.description && <p className={styles.description}>{repo!.description}</p>}
{repo!.description && <p className="text-foreground/85 m-0 text-sm leading-relaxed">{repo!.description}</p>}
<div className={styles.meta}>
<div className="text-muted-foreground mt-2 flex flex-wrap text-sm">
{repo!.primaryLanguage && (
<div className={styles.metaItem}>
<div className="mt-1 mr-5 whitespace-nowrap">
{repo!.primaryLanguage.color && (
<span
className={styles.metaIcon}
className="mr-2 inline-block size-[16px] align-text-top"
style={{ backgroundColor: repo!.primaryLanguage.color, borderRadius: "50%" }}
/>
)}
@ -75,41 +85,35 @@ const Page = async () => {
)}
{repo!.stargazerCount > 0 && (
<div className={styles.metaItem}>
<div className="mt-1 mr-5 whitespace-nowrap">
<Link
href={`${repo!.url}/stargazers`}
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)} ${repo!.stargazerCount === 1 ? "star" : "stars"}`}
plain
className={styles.metaLink}
className="hover:text-primary text-muted-foreground hover:no-underline"
>
<StarIcon size="1.25em" className={styles.metaIcon} />
<StarIcon className="mr-2 inline-block size-[16px] align-text-top" />
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)}</span>
</Link>
</div>
)}
{repo!.forkCount > 0 && (
<div className={styles.metaItem}>
<div className="mt-1 mr-5 whitespace-nowrap">
<Link
href={`${repo!.url}/network/members`}
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)} ${repo!.forkCount === 1 ? "fork" : "forks"}`}
plain
className={styles.metaLink}
className="hover:text-primary text-muted-foreground hover:no-underline"
>
<GitForkIcon size="1.25em" className={styles.metaIcon} />
<GitForkIcon className="mr-2 inline-block size-[16px] align-text-top" />
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)}</span>
</Link>
</div>
)}
<div className={styles.metaItem}>
<div className="mt-1 whitespace-nowrap">
<span
className={styles.metaIcon}
style={{
// invisible icon hack to fix line height
width: 0,
marginRight: 0,
}}
className="mr-0 inline-block h-[16px] w-0 align-text-top"
/>
<span>
Updated <RelativeTime date={repo!.pushedAt} />
@ -120,27 +124,13 @@ const Page = async () => {
))}
</div>
<p
style={{
textAlign: "center",
marginBottom: 0,
fontWeight: 500,
}}
>
<p className="mt-4 mb-0 text-center text-base font-medium">
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}>
View more on{" "}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1.2em"
height="1.2em"
style={{
width: "1.2em",
height: "1.2em",
verticalAlign: "text-top",
margin: "0 0.1em 0 0.25em",
fill: "var(--colors-text)",
}}
className="fill-muted-foreground mx-0.5 inline size-[20px] align-text-top"
>
<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>{" "}

View File

@ -1,4 +1,4 @@
import { env } from "../lib/env";
import { env } from "@/lib/env";
import type { MetadataRoute } from "next";
const robots = (): MetadataRoute.Robots => ({

View File

@ -1,7 +1,7 @@
import { env } from "../lib/env";
import { env } from "@/lib/env";
import path from "path";
import glob from "fast-glob";
import { getFrontMatter } from "../lib/helpers/posts";
import { getFrontMatter } from "@/lib/helpers/posts";
import type { MetadataRoute } from "next";
// routes in /app (in other words, directories containing a page.tsx/mdx file) are automatically included; add a route

View File

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

View File

@ -1,5 +1,6 @@
import PageTitle from "../../components/PageTitle";
import { createMetadata } from "../../lib/helpers/metadata";
import PageTitle from "@/components/page-title";
import Comments from "@/components/comments";
import { createMetadata } from "@/lib/helpers/metadata";
export const metadata = createMetadata({
title: "/uses",
@ -164,3 +165,7 @@ Other geeky stuff:
- 2x [**ecobee3 lite**](https://www.ecobee.com/en-us/smart-thermostats/smart-wifi-thermostat/)
- 2x [**Sonos One**](https://www.sonos.com/en-us/shop/one.html) (with Alexa turned off...hopefully? 🤫)
- 2x [**Apple TV 4K** (2021)](https://www.apple.com/apple-tv-4k/)
---
<Comments title="/uses" />

80
app/zip/page.mdx Normal file
View File

@ -0,0 +1,80 @@
import PageTitle from "@/components/page-title";
import Comments from "@/components/comments";
import { createMetadata } from "@/lib/helpers/metadata";
import backgroundImg from "./sundar.jpg";
export const metadata = createMetadata({
title: "fuckyougoogle.zip 🖕",
description: "This is a horrible idea.",
canonical: "/zip",
});
export const Terminal = () => (
<div
className="relative mx-auto my-4 w-full rounded-lg"
style={{
backgroundImage: `url(${backgroundImg.src})`,
backgroundRepeat: "repeat",
backgroundPosition: "center",
}}
>
<code className="border-ring block overflow-x-auto rounded-lg border border-solid bg-black/60 p-4 text-sm text-white/90 backdrop-blur-xs backdrop-saturate-150">
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>mv</span> /root
<a href="https://killedbygoogle.com/" style={{ color: "inherit" }} className="hover:no-underline">
/stable_products_that_people_rely_on/
</a>
googledomains.zip /tmp/
<br />
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>crontab</span>{" "}
<span style={{ color: "#fd992a" }}>-l</span>
<br />
<br />
<span style={{ color: "#929292" }}>
# TODO(someone else): make super duper sure this only deletes actual zip files and *NOT* the sketchy domains
ending with file extensions released by us & purchased on our registrar (which i just yeeted btw cuz i&apos;m &
also my evil superpowers are fueled by my reckless disregard for the greater good of the internet). - xoxo
sundar <span style={{ color: "#f95757" }}>&lt;3</span>
</span>
<br />
<span style={{ color: "#78df55" }}>@monthly</span>&nbsp;&nbsp;&nbsp;&nbsp;
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span> /tmp/
<a href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} className="hover:no-underline">
*.zip
</a>
<br />
<br />
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>reboot</span> 0
</code>
</div>
);
<PageTitle canonical="https://fuckyougoogle.zip">fuckyougoogle.zip 🖕</PageTitle>
<Terminal />
---
### This reaction seems a little extreme?
A little-known monopolistic internet conglomorate simply unleashed [multiple](https://blog.google/products/registry/8-new-top-level-domains-for-dads-grads-tech/) TLDs with coincidentally matching binary file extentions onto the web and then abruptly [exited the consumer domain registrar business](https://newsroom.squarespace.com/blog/squarespace-domains-updates), what's the big deal?
- **Bobby Rauch:** [The Dangers of Google's .zip TLD](https://medium.com/@bobbyrsec/the-dangers-of-googles-zip-tld-5e1e675e59a5)
- **Fortinet:** [Threat Actors Add .zip Domains to Their Phishing Arsenals](https://www.fortinet.com/blog/industry-trends/threat-actors-add-zip-domains-to-phishing-arsenals)
- **Cisco Talos:** [.Zip top-level domains draw potential for information leaks](https://blog.talosintelligence.com/zip-tld-information-leak/)
- **Ars Technica:** [Google pushes .zip and .mov domains onto the Internet, and the Internet pushes back](https://arstechnica.com/information-technology/2023/05/critics-say-googles-new-zip-and-mov-domains-will-be-a-boon-to-scammers/)
- **Malwarebytes:** [Zip domains, a bad idea nobody asked for](https://www.threatdown.com/blog/zip-domains-a-bad-idea-nobody-asked-for/)
- **Wired:** [The Real Risks in Google's New .Zip and .Mov Domains](https://www.wired.com/story/google-zip-mov-domains-phishing-risks/)
- **Netcraft:** [Phishing attacks already using the .zip TLD](https://www.netcraft.com/blog/phishing-attacks-already-using-the-zip-tld/)
- **Bleeping Computer:** [New ZIP domains spark debate among cybersecurity experts](https://www.bleepingcomputer.com/news/security/new-zip-domains-spark-debate-among-cybersecurity-experts/)
- **Red Canary:** [Broken zippers: Detecting deception with Google's new ZIP domains](https://redcanary.com/blog/threat-detection/google-zip-domains/)
- **Kaspersky:** [Beware the .zip and .mov domains!](https://usa.kaspersky.com/blog/zip-mov-domain-extension-confusion/28351/)
- **Palo Alto Networks:** [New Top Level Domains .zip and .mov open the door for new attacks](https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA14u000000g1wOCAQ)
- **Google, twenty years ago:** ["Don't be evil"](https://web.archive.org/web/20050204181615/http://investor.google.com/conduct.html)
---
<Comments title="fuckyougoogle.zip" />

View File

@ -1,173 +0,0 @@
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import { H3 } from "../../components/Heading";
import HorizontalRule from "../../components/HorizontalRule";
import UnorderedList, { ListItem } from "../../components/List";
import Comments from "../../components/Comments";
import { createMetadata } from "../../lib/helpers/metadata";
import backgroundImg from "./sundar.jpg";
export const metadata = createMetadata({
title: "fuckyougoogle.zip 🖕",
description: "This is a horrible idea.",
canonical: "/zip",
});
const Page = () => {
return (
<>
<PageTitle canonical="https://fuckyougoogle.zip">fuckyougoogle.zip 🖕</PageTitle>
<div
style={{
position: "relative",
width: "100%",
margin: "1em auto",
backgroundImage: `url(${backgroundImg.src})`,
backgroundRepeat: "repeat",
backgroundPosition: "center",
borderRadius: "0.6em",
}}
>
<code
style={{
backgroundColor: "var(--colors-background-header)",
backdropFilter: "saturate(180%) blur(5px))",
display: "block",
overflowX: "auto",
padding: "1em",
fontSize: "0.9em",
tabSize: 2,
border: "1px solid var(--colors-kinda-light)",
borderRadius: "0.6em",
}}
>
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>mv</span> /root
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} plain>
/stable_products_that_people_rely_on/
</Link>
googledomains.zip /tmp/
<br />
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>crontab</span>{" "}
<span style={{ color: "#fd992a" }}>-l</span>
<br />
<br />
<span style={{ color: "#929292" }}>
# TODO(someone else): make super duper sure this only deletes actual zip files and *NOT* the sketchy domains
ending with file extensions released by us & purchased on our registrar (which i just yeeted btw cuz
i&apos;m & also my evil superpowers are fueled by my reckless disregard for the greater good of the
internet). - xoxo sundar <span style={{ color: "#f95757" }}>&lt;3</span>
</span>
<br />
<span style={{ color: "#78df55" }}>@monthly</span>&nbsp;&nbsp;&nbsp;&nbsp;
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span> /tmp/
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} plain>
*.zip
</Link>
<br />
<br />
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>reboot</span> 0
</code>
</div>
<HorizontalRule />
<H3>This reaction seems a little extreme?</H3>
<p>
A little-known monopolistic internet conglomorate simply unleashed{" "}
<Link href="https://blog.google/products/registry/8-new-top-level-domains-for-dads-grads-tech/">multiple</Link>{" "}
TLDs with coincidentally matching binary file extentions onto the web and then abruptly{" "}
<Link href="https://newsroom.squarespace.com/blog/squarespace-domains-updates">
exited the consumer domain registrar business
</Link>
, what&rsquo;s the big deal?
</p>
<UnorderedList>
<ListItem>
<strong>Bobby Rauch:</strong>{" "}
<Link href="https://medium.com/@bobbyrsec/the-dangers-of-googles-zip-tld-5e1e675e59a5">
The Dangers of Google&rsquo;s .zip TLD
</Link>
</ListItem>
<ListItem>
<strong>Fortinet:</strong>{" "}
<Link href="https://www.fortinet.com/blog/industry-trends/threat-actors-add-zip-domains-to-phishing-arsenals">
Threat Actors Add .zip Domains to Their Phishing Arsenals
</Link>
</ListItem>
<ListItem>
<strong>Cisco Talos:</strong>{" "}
<Link href="https://blog.talosintelligence.com/zip-tld-information-leak/">
.Zip top-level domains draw potential for information leaks
</Link>
</ListItem>
<ListItem>
<strong>Ars Technica:</strong>{" "}
<Link href="https://arstechnica.com/information-technology/2023/05/critics-say-googles-new-zip-and-mov-domains-will-be-a-boon-to-scammers/">
Google pushes .zip and .mov domains onto the Internet, and the Internet pushes back
</Link>
</ListItem>
<ListItem>
<strong>Malwarebytes:</strong>{" "}
<Link href="https://www.threatdown.com/blog/zip-domains-a-bad-idea-nobody-asked-for/">
Zip domains, a bad idea nobody asked for
</Link>
</ListItem>
<ListItem>
<strong>Wired:</strong>{" "}
<Link href="https://www.wired.com/story/google-zip-mov-domains-phishing-risks/">
The Real Risks in Google&rsquo;s New .Zip and .Mov Domains
</Link>
</ListItem>
<ListItem>
<strong>Netcraft:</strong>{" "}
<Link href="https://www.netcraft.com/blog/phishing-attacks-already-using-the-zip-tld/">
Phishing attacks already using the .zip TLD
</Link>
</ListItem>
<ListItem>
<strong>Bleeping Computer:</strong>{" "}
<Link href="https://www.bleepingcomputer.com/news/security/new-zip-domains-spark-debate-among-cybersecurity-experts/">
New ZIP domains spark debate among cybersecurity experts
</Link>
</ListItem>
<ListItem>
<strong>Red Canary:</strong>{" "}
<Link href="https://redcanary.com/blog/threat-detection/google-zip-domains/">
Broken zippers: Detecting deception with Google&rsquo;s new ZIP domains
</Link>
</ListItem>
<ListItem>
<strong>Kaspersky:</strong>{" "}
<Link href="https://usa.kaspersky.com/blog/zip-mov-domain-extension-confusion/28351/">
Beware the .zip and .mov domains!
</Link>
</ListItem>
<ListItem>
<strong>Palo Alto Networks:</strong>{" "}
<Link href="https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA14u000000g1wOCAQ">
New Top Level Domains .zip and .mov open the door for new attacks
</Link>
</ListItem>
<ListItem>
<strong>Google, twenty years ago:</strong>{" "}
<Link href="https://web.archive.org/web/20050204181615/http://investor.google.com/conduct.html">
&ldquo;Don&rsquo;t be evil&rdquo;
</Link>
</ListItem>
</UnorderedList>
<HorizontalRule />
<Comments title="fuckyougoogle.zip" />
</>
);
};
export default Page;

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -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);
}

View File

@ -1,12 +0,0 @@
import clsx from "clsx";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Blockquote.module.css";
export type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">;
const Blockquote = ({ className, ...rest }: BlockquoteProps) => (
<blockquote className={clsx(styles.blockquote, className)} {...rest} />
);
export default Blockquote;

View File

@ -1,2 +0,0 @@
export * from "./Blockquote";
export { default } from "./Blockquote";

View File

@ -1,96 +0,0 @@
/**
* Inline code
**/
.code {
padding: 0.2em 0.3em;
font-size: 0.925em;
tab-size: 2px;
page-break-inside: avoid;
background-color: var(--colors-background-header);
border: 1px solid var(--colors-kinda-light);
border-radius: 0.6em;
}
/**
* Syntax-highlighted code blocks
**/
figure:has(.code) {
margin: 1em 0;
position: relative;
width: 100%;
border-radius: 0.6em;
}
figure .code {
display: block;
overflow-x: auto;
padding: 1em;
counter-reset: line;
}
figure .code [style*="--shiki"] {
color: var(--shiki-light);
font-style: var(--shiki-light-font-style);
font-weight: var(--shiki-light-font-weight);
text-decoration: var(--shiki-light-text-decoration);
}
[data-theme="dark"] figure .code [style*="--shiki"] {
color: var(--shiki-dark);
font-style: var(--shiki-dark-font-style);
font-weight: var(--shiki-dark-font-weight);
text-decoration: var(--shiki-dark-text-decoration);
}
figure .code > [data-line]:nth-of-type(1),
figure .code > [data-line]:nth-of-type(2) {
/* excessive right padding to prevent copy button from covering the first two lines of code */
padding-right: 4em;
}
figure .code[data-line-numbers] > [data-line]::before {
display: inline-block;
width: 1em;
margin-right: 1.5em;
text-align: right;
color: var(--colors-medium-light);
user-select: none;
counter-increment: line;
content: counter(line);
}
figure .code[data-line-numbers-max-digits="2"] > [data-line]::before {
width: 1.25rem;
}
figure .code[data-line-numbers-max-digits="3"] > [data-line]::before {
width: 1.75rem;
}
.copyButton {
position: absolute;
top: 0;
right: 0;
height: 3em;
width: 3em;
padding: 0; /* iOS safari fix */
color: var(--colors-medium-dark);
border: 1px solid var(--colors-kinda-light);
border-top-right-radius: 0.6em;
border-bottom-left-radius: 0.6em;
background-color: var(--colors-background-header);
backdrop-filter: saturate(180%) blur(5px);
}
.copyButton > svg {
display: inline-block;
margin: 0 auto;
vertical-align: middle;
}
.copyButton:hover,
.copyButton:focus-visible {
color: var(--colors-link);
}

View File

@ -1,35 +0,0 @@
import clsx from "clsx";
import CopyButton from "../CopyButton";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Code.module.css";
export type CodeProps = ComponentPropsWithoutRef<"code"> & {
"data-language"?: string;
"data-theme"?: string;
};
// a simple wrapper component that "intelligently" picks between inline code and code blocks (w/ optional syntax
// highlighting & a clipboard button)
const Code = ({
"data-language": dataLanguage, // eslint-disable-line @typescript-eslint/no-unused-vars
"data-theme": dataTheme,
className,
children,
...rest
}: CodeProps) => {
return (
<>
{
// detect if this input has been touched by shiki via rehype-pretty-code and if so, include a copy-to-clipboard
// button as its sibling.
dataTheme && <CopyButton className={styles.copyButton} source={children} />
}
<code className={clsx(styles.code, className)} {...rest}>
{children}
</code>
</>
);
};
export default Code;

View File

@ -1,2 +0,0 @@
export * from "./Code";
export { default } from "./Code";

View File

@ -1,2 +0,0 @@
export * from "./CodePen";
export { default } from "./CodePen";

View File

@ -1,2 +0,0 @@
export * from "./Comments";
export { default } from "./Comments";

View File

@ -1,2 +0,0 @@
export * from "./CopyButton";
export { default } from "./CopyButton";

View File

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

View File

@ -1,65 +0,0 @@
import { env } from "../../lib/env";
import clsx from "clsx";
import { HeartIcon } from "lucide-react";
import Link from "../Link";
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 (
<footer className={clsx(styles.footer, className)} {...rest}>
<div className={styles.row}>
<div>
Content{" "}
<Link href="/license" title={config.license} plain className={styles.link}>
licensed under {config.licenseAbbr}
</Link>
,{" "}
<Link href="/previously" title="Previously on..." plain className={styles.link}>
{config.copyrightYearStart}
</Link>{" "}
{new Date().getUTCFullYear()}.
</div>
<div>
Made with <HeartIcon size="1.25em" fill="currentColor" className={clsx(styles.icon, styles.heart)} /> and{" "}
<Link
href="https://nextjs.org/"
title="Powered by Next.js"
aria-label="Next.js"
plain
className={styles.link}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
stroke="currentColor"
strokeWidth="0"
viewBox="0 0 24 24"
width="1.25em"
height="1.25em"
className={styles.icon}
>
<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>
</Link>
.{" "}
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`}
title="View Source on GitHub"
plain
className={clsx(styles.link, styles.underline)}
>
View source.
</Link>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@ -1,2 +0,0 @@
export * from "./Footer";
export { default } from "./Footer";

View File

@ -1,2 +0,0 @@
export * from "./Gist";
export { default } from "./Gist";

View File

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

View File

@ -1,37 +0,0 @@
import Image from "next/image";
import clsx from "clsx";
import Link from "../Link";
import Menu from "../Menu";
import * as config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Header.module.css";
import avatarImg from "../../app/avatar.jpg";
export type HeaderProps = ComponentPropsWithoutRef<"header">;
const Header = ({ className, ...rest }: HeaderProps) => {
return (
<header className={clsx(styles.header, className)} {...rest}>
<nav className={styles.nav}>
<Link dynamicOnHover href="/" rel="author" aria-label={config.authorName} plain className={styles.home}>
<Image
src={avatarImg}
alt={`Photo of ${config.authorName}`}
className={styles.avatar}
width={70}
height={70}
quality={50}
priority
/>
<span className={styles.name}>{config.authorName}</span>
</Link>
<Menu className={styles.menu} />
</nav>
</header>
);
};
export default Header;

View File

@ -1,2 +0,0 @@
export * from "./Header";
export { default } from "./Header";

View File

@ -1,50 +0,0 @@
.h {
margin-top: 1em;
margin-bottom: 0.5em;
line-height: 1.5;
/*
* offset (approximately) with sticky header so jumped-to content isn't hiding behind it.
* note: use rem so it isn't based on the heading's font size.
*/
scroll-margin-top: 5.5rem;
}
.h.divider {
padding-bottom: 0.25em;
border-bottom: 1px solid var(--colors-kinda-light);
}
.anchor {
margin: 0 0.4em;
padding: 0 0.2em;
color: var(--colors-medium) !important;
/* overridden on hover below (except on small screens) */
opacity: 0;
}
/* show anchor link when hovering anywhere over the heading line, or on keyboard tab focus */
.anchor:hover,
.anchor:focus-visible {
color: var(--colors-link) !important;
}
.h:hover .anchor,
.h:focus-visible .anchor {
opacity: 1;
}
@media (max-width: 768px) {
.h {
scroll-margin-top: 5.5rem;
}
.anchor {
margin: 0 0.2em;
padding: 0 0.4em;
/* don't require hover to show anchor link on small (likely touch) screens */
opacity: 1;
}
}

View File

@ -1,35 +0,0 @@
import innerText from "react-innertext";
import clsx from "clsx";
import HeadingAnchor from "../HeadingAnchor";
import type { JSX, ComponentPropsWithoutRef } from "react";
import styles from "./Heading.module.css";
export type HeadingProps = ComponentPropsWithoutRef<"h1"> & {
level: 1 | 2 | 3 | 4 | 5 | 6;
divider?: boolean;
};
const Heading = ({ level, id, divider, className, children, ...rest }: HeadingProps) => {
const HWild: keyof JSX.IntrinsicElements = `h${level}`;
return (
<HWild id={id} className={clsx(styles.h, (divider || level === 2) && styles.divider, className)} {...rest}>
{children}
{/* add anchor link to H2s and H3s. ID is either provided or automatically generated by rehype-slug. */}
{id && (level === 2 || level === 3) && (
<HeadingAnchor id={id} title={`Jump to "${innerText(children)}"`} className={styles.anchor} />
)}
</HWild>
);
};
export const H1 = (props: Omit<HeadingProps, "level">) => <Heading level={1} {...props} />;
export const H2 = (props: Omit<HeadingProps, "level">) => <Heading level={2} {...props} />;
export const H3 = (props: Omit<HeadingProps, "level">) => <Heading level={3} {...props} />;
export const H4 = (props: Omit<HeadingProps, "level">) => <Heading level={4} {...props} />;
export const H5 = (props: Omit<HeadingProps, "level">) => <Heading level={5} {...props} />;
export const H6 = (props: Omit<HeadingProps, "level">) => <Heading level={6} {...props} />;
export default Heading;

View File

@ -1,2 +0,0 @@
export * from "./Heading";
export { default } from "./Heading";

View File

@ -1,17 +0,0 @@
import Link from "../Link";
import { LinkIcon } from "lucide-react";
import type { ComponentPropsWithoutRef } from "react";
export type HeadingAnchorProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href" | "id"> & {
id: string;
};
const HeadingAnchor = ({ id, ...rest }: HeadingAnchorProps) => {
return (
<Link href={`#${id}`} plain {...rest}>
<LinkIcon size="0.8em" />
</Link>
);
};
export default HeadingAnchor;

View File

@ -1,2 +0,0 @@
export * from "./HeadingAnchor";
export { default } from "./HeadingAnchor";

View File

@ -1,7 +0,0 @@
.hr {
margin: 1.5em auto;
max-width: calc(var(--max-width) - 1.5em);
height: 1px;
border: 0;
background-color: var(--colors-light);
}

View File

@ -1,12 +0,0 @@
import clsx from "clsx";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./HorizontalRule.module.css";
export type HorizontalRuleProps = ComponentPropsWithoutRef<"hr">;
const HorizontalRule = ({ className, ...rest }: HorizontalRuleProps) => (
<hr className={clsx(styles.hr, className)} {...rest} />
);
export default HorizontalRule;

View File

@ -1,2 +0,0 @@
export * from "./HorizontalRule";
export { default } from "./HorizontalRule";

View File

@ -1,17 +0,0 @@
.image {
display: block;
margin: 1em auto;
max-width: 100%;
height: auto;
}
/* weird hack to enable image captions in markdown */
.image + em {
display: block;
text-align: center;
margin-top: -0.4em;
font-style: normal;
font-size: 0.95em;
line-height: 1.5;
color: var(--colors-medium);
}

View File

@ -1,30 +0,0 @@
import NextImage from "next/image";
import clsx from "clsx";
import { MAX_WIDTH } from "../../lib/config/constants";
import type { ComponentPropsWithoutRef } from "react";
import type { StaticImageData } from "next/image";
import styles from "./Image.module.css";
export type ImageProps = ComponentPropsWithoutRef<typeof NextImage>;
const Image = ({ src, height, width, placeholder, className, ...rest }: ImageProps) => {
const constrainWidth = (width?: number | `${number}`) => {
if (!width) return MAX_WIDTH;
// ensure that the image width is not greater than the global maximum width
return Math.min(typeof width === "string" ? parseInt(width, 10) : width, MAX_WIDTH);
};
const imageProps: ImageProps = {
src,
height,
width: constrainWidth(width || (src as StaticImageData).width),
placeholder: placeholder || (typeof src === "object" && "blurDataURL" in src ? "blur" : "empty"),
...rest,
};
return <NextImage className={clsx(styles.image, className)} {...imageProps} />;
};
export default Image;

View File

@ -1,2 +0,0 @@
export * from "./Image";
export { default } from "./Image";

View File

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

View File

@ -1,2 +0,0 @@
export * from "./Link";
export { default } from "./Link";

View File

@ -1,8 +0,0 @@
.list {
margin-left: 1.5em;
padding-left: 0;
}
.listItem {
padding-left: 0.25em;
}

View File

@ -1,18 +0,0 @@
import clsx from "clsx";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./List.module.css";
export const UnorderedList = ({ className, ...rest }: ComponentPropsWithoutRef<"ul">) => (
<ul className={clsx(styles.list, className)} {...rest} />
);
export const OrderedList = ({ className, ...rest }: ComponentPropsWithoutRef<"ol">) => (
<ol className={clsx(styles.list, className)} {...rest} />
);
export const ListItem = ({ className, ...rest }: ComponentPropsWithoutRef<"li">) => (
<li className={clsx(styles.listItem, className)} {...rest} />
);
export default UnorderedList;

View File

@ -1,2 +0,0 @@
export * from "./List";
export { default } from "./List";

View File

@ -1,22 +0,0 @@
.loading {
display: inline-block;
text-align: center;
}
.box {
display: inline-block;
height: 100%;
animation: loading 1.5s infinite ease-in-out both;
background-color: var(--colors-medium-light);
}
@keyframes loading {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(0.6);
}
}

View File

@ -1,2 +0,0 @@
export * from "./Loading";
export { default } from "./Loading";

View File

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

View File

@ -1,46 +0,0 @@
"use client";
import { usePathname } from "next/navigation";
import clsx from "clsx";
import MenuItem from "../MenuItem";
import ThemeToggle from "../ThemeToggle";
import { menuItems } from "../../lib/config/menu";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Menu.module.css";
export type MenuProps = ComponentPropsWithoutRef<"ul">;
const Menu = ({ className, ...rest }: MenuProps) => {
const pathname = usePathname() || "";
return (
<ul className={clsx(styles.menu, className)} {...rest}>
{menuItems.map((item, index) => {
// kinda weird/hacky way to determine if the *first part* of the current path matches this href
const isCurrent = item.href === `/${pathname.split("/")[1]}`;
return (
<li className={styles.item} key={item.text || index}>
<MenuItem {...item} current={isCurrent} />
</li>
);
})}
<li
className={styles.item}
style={{
// manually align the theme toggle with the rest of the menu icons
paddingTop: "0.2em",
}}
>
<MenuItem
// @ts-expect-error
icon={ThemeToggle}
/>
</li>
</ul>
);
};
export default Menu;

View File

@ -1,2 +0,0 @@
export * from "./Menu";
export { default } from "./Menu";

View File

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

View File

@ -1,2 +0,0 @@
export * from "./MenuItem";
export { default } from "./MenuItem";

View File

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

View File

@ -1,21 +0,0 @@
import clsx from "clsx";
import Link from "../Link";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./PageTitle.module.css";
export type PageTitleProps = ComponentPropsWithoutRef<"h1"> & {
canonical: string;
};
const PageTitle = ({ canonical, className, children, ...rest }: PageTitleProps) => {
return (
<h1 className={clsx(styles.title, className)} {...rest}>
<Link href={canonical} plain className={styles.slug}>
{children}
</Link>
</h1>
);
};
export default PageTitle;

View File

@ -1,2 +0,0 @@
export * from "./PageTitle";
export { default } from "./PageTitle";

View File

@ -1,2 +0,0 @@
export * from "./RelativeTime";
export { default } from "./RelativeTime";

View File

@ -1,29 +0,0 @@
/*!
* @reach/skip-nav | MIT License | https://github.com/reach/reach-ui/blob/v0.18.0/packages/skip-nav/styles.css
*/
.hidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
position: absolute;
}
.hidden:focus {
padding: 1rem;
position: fixed;
top: 10px;
left: 10px;
z-index: 99999;
width: auto;
height: auto;
clip: auto;
background: var(--colors-super-duper-light);
color: var(--colors-link);
border: 2px solid var(--colors-kinda-light);
text-decoration: underline;
}

View File

@ -1,17 +0,0 @@
import styles from "./SkipNav.module.css";
const skipNavId = "skip-nav";
export const SkipNavLink = () => {
return (
<a href={`#${skipNavId}`} tabIndex={0} className={styles.hidden}>
Skip to content
</a>
);
};
export const SkipNavTarget = () => {
return <div id={skipNavId} />;
};
export default SkipNavLink;

View File

@ -1,2 +0,0 @@
export * from "./SkipNav";
export { default } from "./SkipNav";

View File

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

View File

@ -1,2 +0,0 @@
export * from "./ThemeToggle";
export { default } from "./ThemeToggle";

View File

@ -1,2 +0,0 @@
export * from "./Time";
export { default } from "./Time";

View File

@ -1,8 +0,0 @@
.tweet {
/* help with layout shift */
min-height: 120px;
}
.tweet :global(.react-tweet-theme) {
--tweet-container-margin: 1.5rem auto;
}

View File

@ -1,2 +0,0 @@
export * from "./Tweet";
export { default } from "./Tweet";

View File

@ -1,7 +0,0 @@
.player {
display: block;
margin: 0 auto;
width: 100%;
height: auto;
max-height: 500px;
}

View File

@ -1,2 +0,0 @@
export * from "./Video";
export { default } from "./Video";

View File

@ -1,2 +0,0 @@
export * from "./YouTube";
export { default } from "./YouTube";

Some files were not shown because too many files have changed in this diff Show More