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:
@ -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
1
.npmrc
@ -1,3 +1,2 @@
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=*prettier*
|
||||
public-hoist-pattern[]=*stylelint*
|
||||
|
@ -10,3 +10,4 @@ pnpm-lock.yaml
|
||||
# other
|
||||
public/
|
||||
.devcontainer/devcontainer.json
|
||||
.vscode/
|
||||
|
@ -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;
|
||||
|
@ -1,19 +0,0 @@
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
|
||||
/** @type {import("stylelint").Config} */
|
||||
export default {
|
||||
extends: ["stylelint-config-standard", "stylelint-config-css-modules", "stylelint-prettier/recommended"],
|
||||
rules: {
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null,
|
||||
"media-feature-range-notation": null,
|
||||
"rule-empty-line-before": [
|
||||
"always-multi-line",
|
||||
{
|
||||
except: ["after-single-line-comment"],
|
||||
ignore: ["inside-block"],
|
||||
},
|
||||
],
|
||||
"color-hex-length": "long",
|
||||
},
|
||||
};
|
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"unifiedjs.vscode-mdx"
|
||||
]
|
||||
}
|
16
.vscode/settings.json
vendored
Normal file
16
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.rulers": [
|
||||
120
|
||||
],
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss",
|
||||
"*.mdx": "markdown"
|
||||
},
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"typescript.surveys.enabled": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.tsserver.log": "off",
|
||||
"typescript.updateImportsOnFileMove.enabled": "always"
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
[](https://github.com/jakejarvis/jarv.is)
|
||||
[](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
|
||||
|
||||
|
@ -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) => ({
|
||||
|
@ -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";
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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’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>
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
|
346
app/globals.css
346
app/globals.css
@ -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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--fonts-sans);
|
||||
background-color: var(--colors-background-inner);
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-size: 1em;
|
||||
font-family: var(--fonts-mono);
|
||||
font-variant-ligatures: none; /* i hate them. fwiw. */
|
||||
}
|
||||
@layer components {
|
||||
.markdown {
|
||||
@apply text-[0.925rem] leading-relaxed first:mt-0 last:mb-0 md:text-base;
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
p {
|
||||
@apply my-5;
|
||||
}
|
||||
strong {
|
||||
@apply font-semibold;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
@apply scroll-m-4;
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
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;
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
strong {
|
||||
@apply font-black;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
@apply mt-6 mb-4 text-xl leading-snug font-bold first:mt-0 md:text-2xl;
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
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;
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
code {
|
||||
@apply text-[0.9em];
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
@apply mt-6 mb-2 leading-normal font-semibold;
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-appearance: button;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
. © 2016.
|
||||
|
@ -1,26 +0,0 @@
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.default {
|
||||
width: 100%;
|
||||
padding: 1.5em;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.7;
|
||||
color: var(--colors-text);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.default {
|
||||
font-size: 0.925em;
|
||||
line-height: 1.85;
|
||||
}
|
||||
}
|
@ -1,27 +1,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>
|
||||
|
||||
|
@ -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>{" "}
|
||||
&{" "}
|
||||
<Link href="https://leo.fm/" style={{ fontWeight: 700 }}>
|
||||
<Link href="https://leo.fm/" className="font-bold">
|
||||
Leo Laporte
|
||||
</Link>
|
||||
. © 2007 G4 Media, Inc.
|
||||
|
@ -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",
|
||||
|
@ -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: [
|
||||
{
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
184
app/page.tsx
184
app/page.tsx
@ -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’m Jake. <span className={styles.wave}>👋</span>
|
||||
<>
|
||||
<h1 className="mt-0 mb-2 text-3xl leading-relaxed font-medium">
|
||||
Hi there! I’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’m a frontend web developer based in the{" "}
|
||||
<Link
|
||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&iv_load_policy=3"
|
||||
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
|
||||
lightColor="#fb4d42"
|
||||
darkColor="#ff5146"
|
||||
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’ve improved a bit since then, I think? 🤷</span>
|
||||
. <span className="text-muted-foreground">I’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'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’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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
||||

|
||||
_Previously on the [Cringey Chronicles™](https://web.archive.org/web/20010501000000*/jakejarvis.com) of this website's past..._
|
||||
@ -85,7 +80,8 @@ _Previously on the [Cringey Chronicles™](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))_
|
||||
|
@ -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",
|
||||
|
@ -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) =>
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
@ -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>{" "}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { env } from "../lib/env";
|
||||
import { env } from "@/lib/env";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const robots = (): MetadataRoute.Robots => ({
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
@ -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
80
app/zip/page.mdx
Normal 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'm &
|
||||
also my evil superpowers are fueled by my reckless disregard for the greater good of the internet). - xoxo
|
||||
sundar <span style={{ color: "#f95757" }}><3</span>
|
||||
</span>
|
||||
<br />
|
||||
<span style={{ color: "#78df55" }}>@monthly</span>
|
||||
<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" />
|
173
app/zip/page.tsx
173
app/zip/page.tsx
@ -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'm & also my evil superpowers are fueled by my reckless disregard for the greater good of the
|
||||
internet). - xoxo sundar <span style={{ color: "#f95757" }}><3</span>
|
||||
</span>
|
||||
<br />
|
||||
<span style={{ color: "#78df55" }}>@monthly</span>
|
||||
<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’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’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’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’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">
|
||||
“Don’t be evil”
|
||||
</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<HorizontalRule />
|
||||
|
||||
<Comments title="fuckyougoogle.zip" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
21
components.json
Normal file
21
components.json
Normal 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"
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
.blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1.25em;
|
||||
border-left: 0.25em solid var(--colors-link);
|
||||
color: var(--colors-medium-dark);
|
||||
}
|
@ -1,12 +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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./Blockquote";
|
||||
export { default } from "./Blockquote";
|
@ -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);
|
||||
}
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./Code";
|
||||
export { default } from "./Code";
|
@ -1,2 +0,0 @@
|
||||
export * from "./CodePen";
|
||||
export { default } from "./CodePen";
|
@ -1,2 +0,0 @@
|
||||
export * from "./Comments";
|
||||
export { default } from "./Comments";
|
@ -1,2 +0,0 @@
|
||||
export * from "./CopyButton";
|
||||
export { default } from "./CopyButton";
|
@ -1,88 +0,0 @@
|
||||
.footer {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 1.25em 1.5em;
|
||||
border-top: 1px solid var(--colors-kinda-light);
|
||||
background-color: var(--colors-background-outer);
|
||||
color: var(--colors-medium-dark);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
font-size: 0.8em;
|
||||
line-height: 2.3;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--colors-medium-dark) !important;
|
||||
}
|
||||
|
||||
.link:has(.icon):hover,
|
||||
.link:has(.icon):focus-visible {
|
||||
color: var(--colors-medium) !important;
|
||||
}
|
||||
|
||||
.link.underline {
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid var(--colors-light);
|
||||
}
|
||||
|
||||
.link.underline:hover,
|
||||
.link.underline:focus-visible {
|
||||
border-bottom-color: var(--colors-kinda-light);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
margin: 0 0.1em;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.heart {
|
||||
color: var(--colors-error);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.heart {
|
||||
animation: pulse 10s ease 7.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
2% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
4% {
|
||||
transform: scale(1);
|
||||
}
|
||||
6% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
8% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* pause for ~9 out of 10 seconds */
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer {
|
||||
padding: 1em 1.25em;
|
||||
}
|
||||
|
||||
/* stack columns on left instead of flexboxing across */
|
||||
.row {
|
||||
display: block;
|
||||
}
|
||||
}
|
@ -1,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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./Footer";
|
||||
export { default } from "./Footer";
|
@ -1,2 +0,0 @@
|
||||
export * from "./Gist";
|
||||
export { default } from "./Gist";
|
@ -1,83 +0,0 @@
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 4.5em;
|
||||
padding: 0.7em 1.5em;
|
||||
border-bottom: 1px solid var(--colors-kinda-light);
|
||||
background-color: var(--colors-background-header);
|
||||
|
||||
/* make sticky */
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
|
||||
/* blurry glass-like background effect (except on firefox...?) */
|
||||
backdrop-filter: saturate(180%) blur(5px);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 1px solid var(--colors-light);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.home {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
color: var(--colors-medium-dark) !important;
|
||||
}
|
||||
|
||||
.home:hover,
|
||||
.home:focus-visible {
|
||||
color: var(--colors-link) !important;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0 0.6em;
|
||||
font-size: 1.15em;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0.75em 1.25em;
|
||||
height: 5.9em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.home:hover .avatar,
|
||||
.home:focus-visible .avatar {
|
||||
border-color: var(--colors-link-underline);
|
||||
}
|
||||
|
||||
.name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu {
|
||||
max-width: 325px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.menu {
|
||||
max-width: 225px;
|
||||
}
|
||||
}
|
@ -1,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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./Header";
|
||||
export { default } from "./Header";
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./Heading";
|
||||
export { default } from "./Heading";
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./HeadingAnchor";
|
||||
export { default } from "./HeadingAnchor";
|
@ -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);
|
||||
}
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./HorizontalRule";
|
||||
export { default } from "./HorizontalRule";
|
@ -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);
|
||||
}
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./Image";
|
||||
export { default } from "./Image";
|
@ -1,25 +0,0 @@
|
||||
.link {
|
||||
color: var(--colors-link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* fancy underline effect on hover */
|
||||
.link:not(.plain) {
|
||||
background-image: linear-gradient(var(--colors-link-underline), var(--colors-link-underline));
|
||||
background-position: 0% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0% 2px;
|
||||
transition: background-size 0.2s ease-in-out;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.link:not(.plain):hover,
|
||||
.link:not(.plain):focus-visible {
|
||||
background-size: 100% 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.link:not(.plain) {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./Link";
|
||||
export { default } from "./Link";
|
@ -1,8 +0,0 @@
|
||||
.list {
|
||||
margin-left: 1.5em;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
padding-left: 0.25em;
|
||||
}
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./List";
|
||||
export { default } from "./List";
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./Loading";
|
||||
export { default } from "./Loading";
|
@ -1,36 +0,0 @@
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
margin-left: 1em;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.menu {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.menu {
|
||||
margin-left: 1.4em;
|
||||
}
|
||||
|
||||
/* the home icon is kinda redundant when space is SUPER tight */
|
||||
.item:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./Menu";
|
||||
export { default } from "./Menu";
|
@ -1,43 +0,0 @@
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.6em;
|
||||
margin-top: 0.2em;
|
||||
color: var(--colors-medium-dark) !important;
|
||||
}
|
||||
|
||||
/* indicate active page/section */
|
||||
.link.current {
|
||||
margin-bottom: -0.2em;
|
||||
border-bottom: 0.2em solid var(--colors-link-underline);
|
||||
}
|
||||
|
||||
.link:not(.current):hover,
|
||||
.link:not(.current):focus-visible {
|
||||
margin-bottom: -0.2em;
|
||||
border-bottom: 0.2em solid var(--colors-kinda-light);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 0.7em;
|
||||
font-size: 0.925em;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./MenuItem";
|
||||
export { default } from "./MenuItem";
|
@ -1,15 +0,0 @@
|
||||
.title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.6em;
|
||||
font-size: 2em;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.slug::before {
|
||||
content: "\002E\002F"; /* "./" */
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--colors-medium-light);
|
||||
margin-right: -0.1em;
|
||||
}
|
@ -1,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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./PageTitle";
|
||||
export { default } from "./PageTitle";
|
@ -1,2 +0,0 @@
|
||||
export * from "./RelativeTime";
|
||||
export { default } from "./RelativeTime";
|
@ -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;
|
||||
}
|
@ -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;
|
@ -1,2 +0,0 @@
|
||||
export * from "./SkipNav";
|
||||
export { default } from "./SkipNav";
|
@ -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;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./ThemeToggle";
|
||||
export { default } from "./ThemeToggle";
|
@ -1,2 +0,0 @@
|
||||
export * from "./Time";
|
||||
export { default } from "./Time";
|
@ -1,8 +0,0 @@
|
||||
.tweet {
|
||||
/* help with layout shift */
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.tweet :global(.react-tweet-theme) {
|
||||
--tweet-container-margin: 1.5rem auto;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./Tweet";
|
||||
export { default } from "./Tweet";
|
@ -1,7 +0,0 @@
|
||||
.player {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 500px;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from "./Video";
|
||||
export { default } from "./Video";
|
@ -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
Reference in New Issue
Block a user