mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-07-03 13:06:37 -04:00
Migrate to app router (#2254)
This commit is contained in:
96
app/api/contact/route.ts
Normal file
96
app/api/contact/route.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import fetcher from "../../../lib/helpers/fetcher";
|
||||
import config from "../../../lib/config";
|
||||
import { headers } from "next/headers";
|
||||
import { NextResponse, NextRequest } from "next/server";
|
||||
|
||||
export async function POST(req: NextRequest): Promise<
|
||||
NextResponse<{
|
||||
success?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error?: any;
|
||||
} | null>
|
||||
> {
|
||||
try {
|
||||
// possible weirdness? https://github.com/orgs/vercel/discussions/78#discussioncomment-5089059
|
||||
const data = await req.formData();
|
||||
const headersList = await headers();
|
||||
|
||||
// these are both backups to client-side validations just in case someone squeezes through without them. the codes
|
||||
// are identical so they're caught in the same fashion.
|
||||
if (!data.get("name") || !data.get("email") || !data.get("message")) {
|
||||
// all fields are required
|
||||
throw new Error("missing_data");
|
||||
}
|
||||
if (
|
||||
!data.get("cf-turnstile-response") ||
|
||||
!(await validateCaptcha(
|
||||
data.get("cf-turnstile-response"),
|
||||
headersList.get("x-forwarded-for") || headersList.get("x-real-ip") || ""
|
||||
))
|
||||
) {
|
||||
// either the captcha is wrong or completely missing
|
||||
throw new Error("invalid_captcha");
|
||||
}
|
||||
|
||||
// throw an internal error, not user's fault
|
||||
if (!(await sendMessage(data))) {
|
||||
throw new Error("nodemailer_error");
|
||||
}
|
||||
|
||||
// success! let the client know
|
||||
return NextResponse.json({ success: true }, { status: 201 });
|
||||
} catch (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any
|
||||
) {
|
||||
return NextResponse.json({ error: error.message ?? "Bad request." }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const validateCaptcha = async (formResponse: unknown, ip: string): Promise<unknown> => {
|
||||
const response = await fetcher("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// fallback to dummy secret for testing: https://developers.cloudflare.com/turnstile/troubleshooting/testing/
|
||||
secret: process.env.TURNSTILE_SECRET_KEY || "1x0000000000000000000000000000000AA",
|
||||
response: formResponse,
|
||||
remoteip: ip,
|
||||
}),
|
||||
});
|
||||
|
||||
return response?.success;
|
||||
};
|
||||
|
||||
const sendMessage = async (data: FormData): Promise<boolean> => {
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
// https://resend.com/docs/send-with-nodemailer-smtp
|
||||
host: "smtp.resend.com",
|
||||
secure: true,
|
||||
port: 465,
|
||||
auth: {
|
||||
user: "resend",
|
||||
pass: process.env.RESEND_API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `${data.get("name")} <${process.env.RESEND_DOMAIN ? `noreply@${process.env.RESEND_DOMAIN}` : "onboarding@resend.dev"}>`,
|
||||
sender: `nodemailer <${process.env.RESEND_DOMAIN ? `noreply@${process.env.RESEND_DOMAIN}` : "onboarding@resend.dev"}>`,
|
||||
replyTo: `${data.get("name")} <${data.get("email")}>`,
|
||||
to: `<${config.authorEmail}>`,
|
||||
subject: `[${config.siteDomain}] Contact Form Submission`,
|
||||
// TODO: add markdown parsing as promised on the form.
|
||||
text: `${data.get("message")}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
31
app/api/count/route.ts
Normal file
31
app/api/count/route.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/helpers/prisma";
|
||||
import type { PageStats } from "../../../types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse<PageStats>> {
|
||||
const slug = req.nextUrl.searchParams.get("slug");
|
||||
|
||||
// extremely basic input validation.
|
||||
// TODO: actually check if the note exists before continuing (and allow pages other than notes).
|
||||
if (typeof slug !== "string" || !new RegExp(/^notes\/([A-Za-z0-9-]+)$/i).test(slug)) {
|
||||
// @ts-expect-error
|
||||
return NextResponse.json({ error: "Missing or invalid 'slug' parameter." }, { status: 400 });
|
||||
}
|
||||
|
||||
// +1 hit!
|
||||
const { hits } = await prisma.hits.upsert({
|
||||
where: { slug },
|
||||
create: { slug },
|
||||
update: {
|
||||
hits: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// add one to this page's count and return the new number
|
||||
return NextResponse.json({ hits });
|
||||
}
|
26
app/api/hits/route.ts
Normal file
26
app/api/hits/route.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/helpers/prisma";
|
||||
import type { SiteStats } from "../../../types";
|
||||
|
||||
export const revalidate = 900; // 15 mins
|
||||
|
||||
export async function GET(): Promise<NextResponse<SiteStats>> {
|
||||
// fetch all rows from db sorted by most hits
|
||||
const pages = await prisma.hits.findMany({
|
||||
orderBy: [
|
||||
{
|
||||
hits: "desc",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const total = { hits: 0 };
|
||||
|
||||
// calculate total hits
|
||||
pages.forEach((page) => {
|
||||
// add these hits to running tally
|
||||
total.hits += page.hits;
|
||||
});
|
||||
|
||||
return NextResponse.json({ total, pages });
|
||||
}
|
BIN
app/apple-icon.png
Normal file
BIN
app/apple-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
40
app/birthday/page.tsx
Normal file
40
app/birthday/page.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Video from "../../components/Video";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import thumbnail from "../../public/static/images/birthday/thumb.png";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "🎉 Cranky Birthday Boy on VHS Tape 📼",
|
||||
description: "The origin of my hatred for the Happy Birthday song.",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "🎉 Cranky Birthday Boy on VHS Tape 📼",
|
||||
images: [thumbnail.src],
|
||||
url: "/birthday",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/birthday",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>📼 1996.MOV</PageTitle>
|
||||
|
||||
<Content>
|
||||
<Video
|
||||
src={{
|
||||
webm: "/static/images/birthday/birthday.webm",
|
||||
mp4: "/static/images/birthday/birthday.mp4",
|
||||
image: thumbnail.src,
|
||||
}}
|
||||
/>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
75
app/cli/page.tsx
Normal file
75
app/cli/page.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import Image from "../../components/Image";
|
||||
import Blockquote from "../../components/Blockquote";
|
||||
import CodeBlock from "../../components/CodeBlock";
|
||||
import { H2 } from "../../components/Heading";
|
||||
import { UnorderedList, ListItem } from "../../components/List";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import cliImg from "../../public/static/images/cli/screenshot.png";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "CLI",
|
||||
description: "AKA, the most useless Node module ever published, in history, by anyone, ever.",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "CLI",
|
||||
images: [cliImg.src],
|
||||
url: "/cli",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/cli",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>🤖 CLI</PageTitle>
|
||||
|
||||
<Content>
|
||||
<Blockquote>
|
||||
The <Link href="/">Jake Jarvis</Link> CLI (aka the most useless Node module ever published, in history, by
|
||||
anyone, ever).
|
||||
</Blockquote>
|
||||
|
||||
<Image src={cliImg} href="https://www.npmjs.com/package/@jakejarvis/cli" alt="Terminal Screenshot" priority />
|
||||
|
||||
<H2 id="usage">Usage</H2>
|
||||
<CodeBlock withCopyButton>npx @jakejarvis/cli</CodeBlock>
|
||||
|
||||
<H2 id="inspired-by">Inspired by</H2>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/sindresorhus/sindresorhus-cli">@sindresorhus/sindresorhus-cli</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/yg/ygcodes">@yg/ygcodes</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="built-with">Built with</H2>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/vadimdemedes/ink">ink</Link> - React for interactive command-line apps
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/sindresorhus/meow">meow</Link> - CLI helper
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
<p>
|
||||
<Link href="https://github.com/jakejarvis/jakejarvis/tree/main/cli">View source on GitHub.</Link>
|
||||
</p>
|
||||
|
||||
<H2 id="license">License</H2>
|
||||
<p>
|
||||
MIT © <Link href="/">Jake Jarvis</Link>, <Link href="https://sindresorhus.com">Sindre Sorhus</Link>
|
||||
</p>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
95
app/contact/form.module.css
Normal file
95
app/contact/form.module.css
Normal file
@ -0,0 +1,95 @@
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.8em;
|
||||
margin: 0.6em 0;
|
||||
border: 2px solid var(--colors-light);
|
||||
border-radius: var(--radii-corner);
|
||||
color: var(--colors-text);
|
||||
background-color: var(--colors-superDuperLight);
|
||||
transition: background var(--transitions-fade);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--colors-link);
|
||||
}
|
||||
|
||||
.input.missing {
|
||||
border-color: var(--colors-error);
|
||||
}
|
||||
|
||||
.input.textarea {
|
||||
margin-bottom: 0;
|
||||
line-height: 1.5;
|
||||
min-height: 10em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.markdownTip {
|
||||
font-size: 0.825em;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.markdownIcon {
|
||||
display: inline;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
vertical-align: -0.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.actionRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 3.75em;
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
flex-shrink: 0;
|
||||
height: 3.25em;
|
||||
padding: 1em 1.25em;
|
||||
margin-right: 1.5em;
|
||||
border: 0;
|
||||
border-radius: var(--radii-corner);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
color: var(--colors-text);
|
||||
background-color: var(--colors-kindaLight);
|
||||
}
|
||||
|
||||
.submitButton:hover,
|
||||
.submitButton:focus-visible {
|
||||
color: var(--colors-superDuperLight);
|
||||
background-color: var(--colors-link);
|
||||
}
|
||||
|
||||
.submitIcon {
|
||||
font-size: 1.3em;
|
||||
margin-right: 0.3em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.result {
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.result.success {
|
||||
color: var(--colors-success);
|
||||
}
|
||||
.result.error {
|
||||
color: var(--colors-error);
|
||||
}
|
||||
|
||||
.resultIcon {
|
||||
display: inline;
|
||||
width: 1.3em;
|
||||
height: 1.3em;
|
||||
vertical-align: -0.3em;
|
||||
fill: currentColor;
|
||||
}
|
200
app/contact/form.tsx
Normal file
200
app/contact/form.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import Turnstile from "react-turnstile";
|
||||
import clsx from "clsx";
|
||||
import Link from "../../components/Link";
|
||||
import useTheme from "../../hooks/useTheme";
|
||||
import { GoCheck, GoX } from "react-icons/go";
|
||||
import { SiMarkdown } from "react-icons/si";
|
||||
import type { FormikHelpers, FormikProps, FieldInputProps, FieldMetaProps } from "formik";
|
||||
|
||||
import styles from "./form.module.css";
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
email: string;
|
||||
message: string;
|
||||
"cf-turnstile-response": string;
|
||||
};
|
||||
|
||||
export type ContactFormProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ContactForm = ({ className }: ContactFormProps) => {
|
||||
const { activeTheme } = useTheme();
|
||||
|
||||
// status/feedback:
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [feedback, setFeedback] = useState("");
|
||||
|
||||
const handleSubmit = (values: FormValues, { setSubmitting }: FormikHelpers<FormValues>) => {
|
||||
// once a user attempts a submission, this is true and stays true whether or not the next attempt(s) are successful
|
||||
setSubmitted(true);
|
||||
|
||||
// https://stackoverflow.com/a/68372184
|
||||
const formData = new FormData();
|
||||
for (const key in values) {
|
||||
formData.append(key, values[key as keyof FormValues]);
|
||||
}
|
||||
|
||||
// if we've gotten here then all data is (or should be) valid and ready to post to API
|
||||
fetch("/api/contact/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.success === true) {
|
||||
// handle successful submission
|
||||
// disable submissions, hide the send button, and let user know we were successful
|
||||
setSuccess(true);
|
||||
setFeedback("Thanks! You should hear from me soon.");
|
||||
} else {
|
||||
// pass on any error sent by the server to the catch block below
|
||||
throw new Error(data.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setSuccess(false);
|
||||
|
||||
if (error.message === "missing_data") {
|
||||
// this should be validated client-side but it's also checked server-side just in case someone slipped past
|
||||
setFeedback("Please make sure that all fields are properly filled in.");
|
||||
} else if (error.message === "invalid_captcha") {
|
||||
// missing/invalid captcha
|
||||
setFeedback("Did you complete the CAPTCHA? (If you're human, that is...)");
|
||||
} else {
|
||||
// something else went wrong, and it's probably my fault...
|
||||
setFeedback("Internal server error... Try again later or shoot me an old-fashioned email?");
|
||||
}
|
||||
})
|
||||
.finally(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={{
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
"cf-turnstile-response": "",
|
||||
}}
|
||||
validate={(values: FormValues) => {
|
||||
const errors: Partial<Record<keyof FormValues, boolean>> = {};
|
||||
|
||||
errors.name = !values.name;
|
||||
errors.email = !values.email; // also loosely validated that it's email-like via browser (not foolproof)
|
||||
errors.message = !values.message;
|
||||
errors["cf-turnstile-response"] = !values["cf-turnstile-response"];
|
||||
|
||||
if (!errors.name && !errors.email && !errors.message && !errors["cf-turnstile-response"]) {
|
||||
setFeedback("");
|
||||
return {};
|
||||
} else {
|
||||
setSuccess(false);
|
||||
setFeedback("Please make sure that all fields are properly filled in.");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}}
|
||||
>
|
||||
{({ setFieldValue, isSubmitting }: FormikProps<FormValues>) => (
|
||||
<Form className={className} name="contact">
|
||||
<Field name="name">
|
||||
{({ field, meta }: { field: FieldInputProps<string>; meta: FieldMetaProps<string> }) => (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
disabled={success}
|
||||
className={clsx(styles.input, { [styles.missing]: !!(meta.error && meta.touched) })}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field name="email">
|
||||
{({ field, meta }: { field: FieldInputProps<string>; meta: FieldMetaProps<string> }) => (
|
||||
<input
|
||||
type="email"
|
||||
inputMode="email"
|
||||
placeholder="Email"
|
||||
disabled={success}
|
||||
className={clsx(styles.input, { [styles.missing]: !!(meta.error && meta.touched) })}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field name="message">
|
||||
{({ field, meta }: { field: FieldInputProps<string>; meta: FieldMetaProps<string> }) => (
|
||||
<TextareaAutosize
|
||||
placeholder="Write something..."
|
||||
minRows={5}
|
||||
disabled={success}
|
||||
className={clsx(styles.input, { [styles.missing]: !!(meta.error && meta.touched) })}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div className={styles.markdownTip}>
|
||||
<SiMarkdown className={styles.markdownIcon} /> Basic{" "}
|
||||
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" style={{ fontWeight: 600 }}>
|
||||
Markdown syntax
|
||||
</Link>{" "}
|
||||
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
|
||||
<Link href="https://jarv.is" underline={false} openInNewTab>
|
||||
links
|
||||
</Link>
|
||||
](https://jarv.is), and <code>`code`</code>.
|
||||
</div>
|
||||
|
||||
<Turnstile
|
||||
sitekey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "1x00000000000000000000AA"}
|
||||
onVerify={(token) => setFieldValue("cf-turnstile-response", token)}
|
||||
className={styles.captcha}
|
||||
theme={activeTheme === "dark" ? activeTheme : "light"}
|
||||
/>
|
||||
|
||||
<div className={styles.actionRow}>
|
||||
<button
|
||||
type="submit"
|
||||
title="Send Message"
|
||||
aria-label="Send Message"
|
||||
onClick={() => setSubmitted(true)}
|
||||
disabled={isSubmitting}
|
||||
className={styles.submitButton}
|
||||
style={{ display: success ? "none" : "inline-flex" }}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span>Sending...</span>
|
||||
) : (
|
||||
<>
|
||||
<span className={styles.submitIcon}>📤</span> <span>Send</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={clsx(styles.result, success ? styles.success : styles.error)}
|
||||
style={{ display: submitted && feedback && !isSubmitting ? "block" : "none" }}
|
||||
>
|
||||
{success ? <GoCheck className={styles.resultIcon} /> : <GoX className={styles.resultIcon} />} {feedback}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactForm;
|
52
app/contact/page.tsx
Normal file
52
app/contact/page.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import ContactForm from "./form";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata, Route } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Contact Me",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "Contact Me",
|
||||
url: "/contact",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/contact",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>📬 Contact Me</PageTitle>
|
||||
|
||||
<Content
|
||||
style={{
|
||||
maxWidth: "600px",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
Fill out this quick form and I'll get back to you as soon as I can! You can also{" "}
|
||||
<Link href="mailto:jake@jarv.is">email me directly</Link>, send me a{" "}
|
||||
<Link href="https://fediverse.jarv.is/@jake">direct message on Mastodon</Link>, or{" "}
|
||||
<Link href="sms:+1-617-917-3737">text me</Link>.
|
||||
</p>
|
||||
<p>
|
||||
🔐 You can grab my public key here:{" "}
|
||||
<Link href={"/pubkey.asc" as Route} title="My Public PGP Key" rel="pgpkey authn" openInNewTab>
|
||||
<code style={{ fontSize: "0.925em", letterSpacing: "0.075em", wordSpacing: "-0.3em" }}>
|
||||
6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39
|
||||
</code>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<ContactForm />
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
11
app/feed.atom/route.ts
Normal file
11
app/feed.atom/route.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { buildFeed } from "../../lib/helpers/build-feed";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export const GET = async () => {
|
||||
return new Response(await buildFeed({ type: "atom" }), {
|
||||
headers: {
|
||||
"content-type": "application/atom+xml; charset=utf-8",
|
||||
},
|
||||
});
|
||||
};
|
11
app/feed.xml/route.ts
Normal file
11
app/feed.xml/route.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { buildFeed } from "../../lib/helpers/build-feed";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export const GET = async () => {
|
||||
return new Response(await buildFeed({ type: "rss" }), {
|
||||
headers: {
|
||||
"content-type": "application/rss+xml; charset=utf-8",
|
||||
},
|
||||
});
|
||||
};
|
21
app/global.css
Normal file
21
app/global.css
Normal file
@ -0,0 +1,21 @@
|
||||
body {
|
||||
font-family: var(--fonts-sans);
|
||||
background-color: var(--colors-backgroundInner);
|
||||
transition: background var(--transitions-fade);
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: var(--fonts-mono);
|
||||
}
|
||||
|
||||
/* https://css-tricks.com/almanac/rules/m/media/prefers-reduced-motion/ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
}
|
66
app/hillary/page.tsx
Normal file
66
app/hillary/page.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import Video from "../../components/Video";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import thumbnail from "../../public/static/images/hillary/thumb.png";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "My Brief Apperance in Hillary Clinton's DNC Video",
|
||||
description: "My brief apperance in one of Hillary Clinton's 2016 DNC convention videos on substance abuse.",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "My Brief Apperance in Hillary Clinton's DNC Video",
|
||||
images: [thumbnail.src],
|
||||
url: "/hillary",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/hillary",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>My Brief Apperance in Hillary Clinton's DNC Video</PageTitle>
|
||||
|
||||
<Content>
|
||||
<Video
|
||||
src={{
|
||||
webm: "/static/images/hillary/convention-720p.webm",
|
||||
mp4: "/static/images/hillary/convention-720p.mp4",
|
||||
vtt: "/static/images/hillary/subs.en.vtt",
|
||||
image: thumbnail.src,
|
||||
}}
|
||||
/>
|
||||
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: "0.9em",
|
||||
lineHeight: 1.8,
|
||||
margin: "1.25em 1em 0 1em",
|
||||
color: "var(--colors-mediumLight)",
|
||||
}}
|
||||
>
|
||||
Video is property of{" "}
|
||||
<Link href="https://www.hillaryclinton.com/" style={{ fontWeight: 700 }}>
|
||||
Hillary for America
|
||||
</Link>
|
||||
, the{" "}
|
||||
<Link href="https://democrats.org/" style={{ fontWeight: 700 }}>
|
||||
Democratic National Committee
|
||||
</Link>
|
||||
, and{" "}
|
||||
<Link href="https://cnnpressroom.blogs.cnn.com/" style={{ fontWeight: 700 }}>
|
||||
CNN / WarnerMedia
|
||||
</Link>
|
||||
. © 2016.
|
||||
</p>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
BIN
app/icon.png
Normal file
BIN
app/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
95
app/layout.tsx
Normal file
95
app/layout.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import clsx from "clsx";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { ThemeProvider } from "../contexts/ThemeContext";
|
||||
import Layout from "../components/Layout";
|
||||
import config from "../lib/config";
|
||||
import type { Metadata } from "next";
|
||||
import type { Person, WithContext } from "schema-dts";
|
||||
|
||||
import { GeistMono, GeistSans } from "../lib/styles/fonts";
|
||||
import "modern-normalize/modern-normalize.css"; // https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css
|
||||
import "./themes.css";
|
||||
import "./global.css";
|
||||
|
||||
import { meJpg } from "../lib/config/favicons";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || `https://${config.siteDomain}`),
|
||||
title: {
|
||||
template: `%s – ${config.siteName}`,
|
||||
default: `${config.siteName} – ${config.shortDescription}`,
|
||||
},
|
||||
description: config.longDescription,
|
||||
openGraph: {
|
||||
siteName: config.siteName,
|
||||
title: {
|
||||
template: `%s – ${config.siteName}`,
|
||||
default: `${config.siteName} – ${config.shortDescription}`,
|
||||
},
|
||||
url: "/",
|
||||
locale: config.siteLocale?.replace("-", "_"),
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: meJpg.src,
|
||||
alt: `${config.siteName} – ${config.shortDescription}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
alternates: {
|
||||
types: {
|
||||
"application/rss+xml": "/feed.xml",
|
||||
"application/atom+xml": "/feed.atom",
|
||||
},
|
||||
canonical: "/",
|
||||
},
|
||||
other: {
|
||||
humans: "/humans.txt",
|
||||
},
|
||||
};
|
||||
|
||||
// https://nextjs.org/docs/app/building-your-application/optimizing/metadata#json-ld
|
||||
const jsonLd: WithContext<Person> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
name: config.authorName,
|
||||
url: metadata.metadataBase?.href || `https://${config.siteDomain}/`,
|
||||
image: new URL(meJpg.src, metadata.metadataBase || `https://${config.siteDomain}`).href,
|
||||
sameAs: [
|
||||
metadata.metadataBase?.href || `https://${config.siteDomain}/`,
|
||||
`https://github.com/${config.authorSocial?.github}`,
|
||||
`https://keybase.io/${config.authorSocial?.keybase}`,
|
||||
`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://${config.authorSocial?.mastodon}`,
|
||||
`https://bsky.app/profile/${config.authorSocial?.bluesky}`,
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang={config.siteLocale} suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
// unminified: https://gist.github.com/jakejarvis/79b0ec8506bc843023546d0d29861bf0
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(()=>{try{const e=document.documentElement,t="undefined"!=typeof Storage?window.localStorage.getItem("theme"):null,a=(t&&"dark"===t)??window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";e.dataset.theme=a,e.style.colorScheme=a}catch(e){}})()`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||
</head>
|
||||
|
||||
<body className={clsx(GeistMono.variable, GeistSans.variable)}>
|
||||
<ThemeProvider>
|
||||
<Layout>{children}</Layout>
|
||||
</ThemeProvider>
|
||||
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
62
app/leo/page.tsx
Normal file
62
app/leo/page.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import Video from "../../components/Video";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import thumbnail from "../../public/static/images/leo/thumb.png";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Facebook App on "The Lab with Leo Laporte"',
|
||||
description: "Powncer app featured in Leo Laporte's TechTV show.",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: 'Facebook App on "The Lab with Leo Laporte"',
|
||||
images: [thumbnail.src],
|
||||
url: "/leo",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/leo",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>Facebook App on "The Lab with Leo Laporte"</PageTitle>
|
||||
|
||||
<Content>
|
||||
<Video
|
||||
src={{
|
||||
webm: "/static/images/leo/leo.webm",
|
||||
mp4: "/static/images/leo/leo.mp4",
|
||||
vtt: "/static/images/leo/subs.en.vtt",
|
||||
image: thumbnail.src,
|
||||
}}
|
||||
/>
|
||||
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: "0.9em",
|
||||
lineHeight: 1.8,
|
||||
margin: "1.25em 1em 0 1em",
|
||||
color: "var(--colors-mediumLight)",
|
||||
}}
|
||||
>
|
||||
Video is property of{" "}
|
||||
<Link href="https://web.archive.org/web/20070511004304/www.g4techtv.ca" style={{ fontWeight: 700 }}>
|
||||
G4techTV Canada
|
||||
</Link>{" "}
|
||||
&{" "}
|
||||
<Link href="https://leolaporte.com/" style={{ fontWeight: 700 }}>
|
||||
Leo Laporte
|
||||
</Link>
|
||||
. © 2007 G4 Media, Inc.
|
||||
</p>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
481
app/license/page.tsx
Normal file
481
app/license/page.tsx
Normal file
@ -0,0 +1,481 @@
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import HorizontalRule from "../../components/HorizontalRule";
|
||||
import Blockquote from "../../components/Blockquote";
|
||||
import { H2, H3 } from "../../components/Heading";
|
||||
import { UnorderedList, OrderedList, ListItem } from "../../components/List";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "License",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "License",
|
||||
url: "/license",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/license",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>📜 License</PageTitle>
|
||||
|
||||
<Content>
|
||||
<p>
|
||||
Unless otherwise noted, content on this website is published under the{" "}
|
||||
<Link href="https://creativecommons.org/licenses/by/4.0/">
|
||||
<strong>Creative Commons Attribution 4.0 International Public License</strong>
|
||||
</Link>{" "}
|
||||
(CC-BY-4.0), which means that you can copy, redistribute, remix, transform, and build upon the content for any
|
||||
purpose as long as you give appropriate credit (such as a hyperlink to the original URL).
|
||||
</p>
|
||||
<p>
|
||||
The <Link href="https://creativecommons.org/licenses/by/4.0/legalcode">full license</Link> is re-printed
|
||||
below.
|
||||
</p>
|
||||
|
||||
<HorizontalRule />
|
||||
|
||||
<H2 id="full-text">Creative Commons Attribution 4.0 International Public License</H2>
|
||||
|
||||
<p style={{ textAlign: "center", lineHeight: 0 }}>
|
||||
<Link
|
||||
href="https://creativecommons.org/licenses/by/4.0/"
|
||||
title="Creative Commons Attribution 4.0"
|
||||
underline={false}
|
||||
>
|
||||
<svg width="120" height="42">
|
||||
<path d="M3.1.5l113.4.2c1.6 0 3-.2 3 3.2l-.1 37.3H.3V3.7C.3 2.1.4.5 3 .5z" fill="#aab2ab"></path>
|
||||
<path d="M117.8 0H2.2C1 0 0 1 0 2.2v39.3c0 .3.2.5.5.5h119c.3 0 .5-.2.5-.5V2.2c0-1.2-1-2.2-2.2-2.2zM2.2 1h115.6c.6 0 1.2.6 1.2 1.2v27.3H36.2a17.8 17.8 0 01-31.1 0H1V2.2C1 1.6 1.5 1 2.1 1z"></path>
|
||||
<path
|
||||
d="M73.8 32.7l.9.1.6.3.5.5.1.8c0 .3 0 .6-.2.8l-.7.6c.4 0 .7.3 1 .6l.2 1-.1 1-.6.5-.7.4H70.7v-6.6h3.1zm-.2 2.7c.3 0 .5 0 .7-.2l.2-.6v-.3l-.3-.3H74l-.4-.1h-1.4v1.5h1.5zm.1 2.8h.4l.4-.1.2-.3v-.4c0-.4 0-.6-.2-.8l-.8-.2h-1.6v1.8h1.6zM76.5 32.7h1.6l1.6 2.7 1.5-2.7H83l-2.5 4.1v2.6h-1.5v-2.6l-2.4-4zM34.3 19.6a13.6 13.6 0 01-27.3 0 13.6 13.6 0 0127.3 0z"
|
||||
fill="#fff"
|
||||
></path>
|
||||
<path d="M31.7 8.5c3 3 4.5 6.7 4.5 11.1a15.4 15.4 0 01-15.6 15.6 15 15 0 01-11-4.6 15 15 0 01-4.6-11c0-4.3 1.5-8 4.6-11.1 3-3 6.7-4.5 11-4.5 4.4 0 8 1.5 11.1 4.5zm-20 2a12.5 12.5 0 00-3.9 9.1c0 3.5 1.3 6.5 3.8 9s5.6 3.8 9 3.8c3.5 0 6.6-1.3 9.2-3.8a12 12 0 003.6-9c0-3.6-1.2-6.6-3.7-9a12.3 12.3 0 00-9-3.8c-3.6 0-6.6 1.2-9 3.7zm6.7 7.6c-.4-.9-1-1.3-1.8-1.3-1.4 0-2 1-2 2.8 0 1.8.6 2.8 2 2.8 1 0 1.6-.5 2-1.4l1.9 1a4.4 4.4 0 01-4.1 2.5c-1.4 0-2.5-.5-3.4-1.3-.8-.9-1.3-2-1.3-3.6 0-1.5.5-2.7 1.3-3.5 1-1 2-1.3 3.3-1.3 2 0 3.3.7 4.1 2.2l-2 1zm9 0c-.4-.9-1-1.3-1.8-1.3-1.4 0-2 1-2 2.8 0 1.8.6 2.8 2 2.8 1 0 1.6-.5 2-1.4l2 1a4.4 4.4 0 01-4.2 2.5c-1.4 0-2.5-.5-3.3-1.3-.9-.9-1.3-2-1.3-3.6 0-1.5.4-2.7 1.3-3.5.8-1 2-1.3 3.2-1.3 2 0 3.3.7 4.2 2.2l-2.1 1z"></path>
|
||||
<g transform="matrix(.99377 0 0 .99367 -177.7 0)">
|
||||
<circle cx="255.6" cy="15.3" r="10.8" fill="#fff"></circle>
|
||||
<path d="M258.7 12.2c0-.4-.4-.8-.8-.8h-4.7c-.5 0-.8.4-.8.8V17h1.3v5.6h3.6V17h1.4v-4.8z"></path>
|
||||
<circle cx="255.5" cy="9.2" r="1.6"></circle>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M255.5 3.4c-3.2 0-6 1.1-8.2 3.4A11.4 11.4 0 00244 15c0 3.2 1.1 6 3.4 8.2 2.3 2.3 5 3.4 8.2 3.4 3.2 0 6-1.1 8.4-3.4a11 11 0 003.3-8.2c0-3.3-1.1-6-3.4-8.3-2.2-2.3-5-3.4-8.3-3.4zm0 2.1c2.7 0 5 1 6.8 2.8a9.2 9.2 0 012.8 6.8c0 2.7-1 4.9-2.7 6.7-2 1.9-4.2 2.8-6.8 2.8-2.7 0-5-1-6.8-2.8A9.2 9.2 0 01246 15c0-2.6 1-4.9 2.8-6.8a9 9 0 016.8-2.8z"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<Blockquote>
|
||||
<p>
|
||||
<em>
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or
|
||||
legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other
|
||||
relationship. Creative Commons makes its licenses and related information available on an "as-is" basis.
|
||||
Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and
|
||||
conditions, or any related information. Creative Commons disclaims all liability for damages resulting
|
||||
from their use to the fullest extent possible.
|
||||
</em>
|
||||
</p>
|
||||
</Blockquote>
|
||||
|
||||
<H3>Using Creative Commons Public Licenses</H3>
|
||||
|
||||
<p>
|
||||
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights
|
||||
holders may use to share original works of authorship and other material subject to copyright and certain
|
||||
other rights specified in the public license below. The following considerations are for informational
|
||||
purposes only, are not exhaustive, and do not form part of our licenses.
|
||||
</p>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<p>
|
||||
<strong>Considerations for licensors:</strong> Our public licenses are intended for use by those
|
||||
authorized to give the public permission to use material in ways otherwise restricted by copyright and
|
||||
certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and
|
||||
conditions of the license they choose before applying it. Licensors should also secure all rights
|
||||
necessary before applying our licenses so that the public can reuse the material as expected. Licensors
|
||||
should clearly mark any material not subject to the license. This includes other CC-licensed material, or
|
||||
material used under an exception or limitation to copyright.{" "}
|
||||
<Link href="https://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors">
|
||||
More considerations for licensors
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
<strong>Considerations for the public:</strong> By using one of our public licenses, a licensor grants the
|
||||
public permission to use the licensed material under specified terms and conditions. If the licensor's
|
||||
permission is not necessary for any reason–for example, because of any applicable exception or limitation
|
||||
to copyright–then that use is not regulated by the license. Our licenses grant only permissions under
|
||||
copyright and certain other rights that a licensor has authority to grant. Use of the licensed material
|
||||
may still be restricted for other reasons, including because others have copyright or other rights in the
|
||||
material. A licensor may make special requests, such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to respect those requests where reasonable.{" "}
|
||||
<Link href="https://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees">
|
||||
More considerations for the public
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H3>Licensed Rights</H3>
|
||||
|
||||
<p>
|
||||
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and
|
||||
conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the
|
||||
extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in
|
||||
consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in
|
||||
consideration of benefits the Licensor receives from making the Licensed Material available under these terms
|
||||
and conditions.
|
||||
</p>
|
||||
|
||||
<H3>Section 1 – Definitions.</H3>
|
||||
|
||||
<p>
|
||||
a. <strong>Adapted Material</strong> means material subject to Copyright and Similar Rights that is derived
|
||||
from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged,
|
||||
transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights
|
||||
held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work,
|
||||
performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in
|
||||
timed relation with a moving image.
|
||||
</p>
|
||||
<p>
|
||||
b. <strong>Adapter's License</strong> means the license You apply to Your Copyright and Similar Rights in Your
|
||||
contributions to Adapted Material in accordance with the terms and conditions of this Public License.
|
||||
</p>
|
||||
<p>
|
||||
c. <strong>Copyright and Similar Rights</strong> means copyright and/or similar rights closely related to
|
||||
copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the
|
||||
rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
||||
</p>
|
||||
<p>
|
||||
d. <strong>Effective Technological Measures</strong> means those measures that, in the absence of proper
|
||||
authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international agreements.
|
||||
</p>
|
||||
<p>
|
||||
e. <strong>Exceptions and Limitations</strong> means fair use, fair dealing, and/or any other exception or
|
||||
limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
|
||||
</p>
|
||||
<p>
|
||||
f. <strong>Licensed Material</strong> means the artistic or literary work, database, or other material to
|
||||
which the Licensor applied this Public License.
|
||||
</p>
|
||||
<p>
|
||||
g. <strong>Licensed Rights</strong> means the rights granted to You subject to the terms and conditions of
|
||||
this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
</p>
|
||||
<p>
|
||||
h. <strong>Licensor</strong> means the individual(s) or entity(ies) granting rights under this Public License.
|
||||
</p>
|
||||
<p>
|
||||
i. <strong>Share</strong> means to provide material to the public by any means or process that requires
|
||||
permission under the Licensed Rights, such as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material available to the public including in ways
|
||||
that members of the public may access the material from a place and at a time individually chosen by them.
|
||||
</p>
|
||||
<p>
|
||||
j. <strong>Sui Generis Database Rights</strong> means rights other than copyright resulting from Directive
|
||||
96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
|
||||
</p>
|
||||
<p>
|
||||
k. <strong>You</strong> means the individual or entity exercising the Licensed Rights under this Public
|
||||
License. <strong>Your</strong> has a corresponding meaning.
|
||||
</p>
|
||||
|
||||
<H3>Section 2 – Scope.</H3>
|
||||
|
||||
<p>
|
||||
a.{" "}
|
||||
<em>
|
||||
<strong>License grant.</strong>
|
||||
</em>
|
||||
</p>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<p>
|
||||
Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide,
|
||||
royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the
|
||||
Licensed Material to:
|
||||
</p>
|
||||
<p>A. reproduce and Share the Licensed Material, in whole or in part; and</p>
|
||||
<p>B. produce, reproduce, and Share Adapted Material.</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
<strong>Exceptions and Limitations.</strong> For the avoidance of doubt, where Exceptions and Limitations
|
||||
apply to Your use, this Public License does not apply, and You do not need to comply with its terms and
|
||||
conditions.
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
<strong>Term.</strong> The term of this Public License is specified in Section 6(a).
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
<strong>Media and formats; technical modifications allowed.</strong> The Licensor authorizes You to
|
||||
exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make
|
||||
technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications necessary to exercise the Licensed Rights,
|
||||
including technical modifications necessary to circumvent Effective Technological Measures. For purposes
|
||||
of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces
|
||||
Adapted Material.
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
<strong>Downstream recipients.</strong>
|
||||
</p>
|
||||
<p>
|
||||
A. <strong>Offer from the Licensor – Licensed Material.</strong> Every recipient of the Licensed Material
|
||||
automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and
|
||||
conditions of this Public License.
|
||||
</p>
|
||||
<p>
|
||||
B. <strong>No downstream restrictions.</strong> You may not offer or impose any additional or different
|
||||
terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing
|
||||
so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
<strong>No endorsement.</strong> Nothing in this Public License constitutes or may be construed as
|
||||
permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with,
|
||||
or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive
|
||||
attribution as provided in Section 3(a)(1)(A)(i).
|
||||
</p>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
<p>
|
||||
b.{" "}
|
||||
<em>
|
||||
<strong>Other rights.</strong>
|
||||
</em>
|
||||
</p>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<p>
|
||||
Moral rights, such as the right of integrity, are not licensed under this Public License, nor are
|
||||
publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor
|
||||
waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to
|
||||
allow You to exercise the Licensed Rights, but not otherwise.
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>Patent and trademark rights are not licensed under this Public License.</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of
|
||||
the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable
|
||||
statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to
|
||||
collect such royalties.
|
||||
</p>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
|
||||
<H3>Section 3 – License Conditions.</H3>
|
||||
|
||||
<p>Your exercise of the Licensed Rights is expressly made subject to the following conditions.</p>
|
||||
<p>
|
||||
a.{" "}
|
||||
<em>
|
||||
<strong>Attribution.</strong>
|
||||
</em>
|
||||
</p>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<p>If You Share the Licensed Material (including in modified form), You must:</p>
|
||||
<p>A. retain the following if it is supplied by the Licensor with the Licensed Material:</p>
|
||||
<p>
|
||||
i. identification of the creator(s) of the Licensed Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
|
||||
</p>
|
||||
<p>ii. a copyright notice;</p>
|
||||
<p>iii. a notice that refers to this Public License;</p>
|
||||
<p>iv. a notice that refers to the disclaimer of warranties;</p>
|
||||
<p>v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;</p>
|
||||
<p>
|
||||
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications;
|
||||
and
|
||||
</p>
|
||||
<p>
|
||||
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the
|
||||
URI or hyperlink to, this Public License.
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and
|
||||
context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the
|
||||
conditions by providing a URI or hyperlink to a resource that includes the required information.
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the
|
||||
extent reasonably practicable.
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>
|
||||
If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of
|
||||
the Adapted Material from complying with this Public License.
|
||||
</p>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
|
||||
<H3>Section 4 – Sui Generis Database Rights.</H3>
|
||||
|
||||
<p>
|
||||
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
|
||||
</p>
|
||||
<p>
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share
|
||||
all or a substantial portion of the contents of the database;
|
||||
</p>
|
||||
<p>
|
||||
b. if You include all or a substantial portion of the database contents in a database in which You have Sui
|
||||
Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its
|
||||
individual contents) is Adapted Material; and
|
||||
</p>
|
||||
<p>
|
||||
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the
|
||||
contents of the database.
|
||||
</p>
|
||||
<p>
|
||||
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public
|
||||
License where the Licensed Rights include other Copyright and Similar Rights.
|
||||
</p>
|
||||
|
||||
<H3>Section 5 – Disclaimer of Warranties and Limitation of Liability.</H3>
|
||||
|
||||
<p>
|
||||
a.{" "}
|
||||
<strong>
|
||||
Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the
|
||||
Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning
|
||||
the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation,
|
||||
warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent
|
||||
or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable.
|
||||
Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
b.{" "}
|
||||
<strong>
|
||||
To the extent possible, in no event will the Licensor be liable to You on any legal theory (including,
|
||||
without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential,
|
||||
punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use
|
||||
of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs,
|
||||
expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may
|
||||
not apply to You.
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner
|
||||
that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
|
||||
</p>
|
||||
|
||||
<H3>Section 6 – Term and Termination.</H3>
|
||||
|
||||
<p>
|
||||
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You
|
||||
fail to comply with this Public License, then Your rights under this Public License terminate automatically.
|
||||
</p>
|
||||
<p>b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:</p>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<p>
|
||||
automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery
|
||||
of the violation; or
|
||||
</p>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<p>upon express reinstatement by the Licensor.</p>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
<p>
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies
|
||||
for Your violations of this Public License.
|
||||
</p>
|
||||
<p>
|
||||
c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or
|
||||
conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this
|
||||
Public License.
|
||||
</p>
|
||||
<p>d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.</p>
|
||||
|
||||
<H3>Section 7 – Other Terms and Conditions.</H3>
|
||||
|
||||
<p>
|
||||
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You
|
||||
unless expressly agreed.
|
||||
</p>
|
||||
<p>
|
||||
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are
|
||||
separate from and independent of the terms and conditions of this Public License.
|
||||
</p>
|
||||
|
||||
<H3>Section 8 – Interpretation.</H3>
|
||||
|
||||
<p>
|
||||
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit,
|
||||
restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without
|
||||
permission under this Public License.
|
||||
</p>
|
||||
<p>
|
||||
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be
|
||||
automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be
|
||||
reformed, it shall be severed from this Public License without affecting the enforceability of the remaining
|
||||
terms and conditions.
|
||||
</p>
|
||||
<p>
|
||||
c. No term or condition of this Public License will be waived and no failure to comply consented to unless
|
||||
expressly agreed to by the Licensor.
|
||||
</p>
|
||||
<p>
|
||||
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any
|
||||
privileges and immunities that apply to the Licensor or You, including from the legal processes of any
|
||||
jurisdiction or authority.
|
||||
</p>
|
||||
|
||||
<Blockquote>
|
||||
<p>
|
||||
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply
|
||||
one of its public licenses to material it publishes and in those instances will be considered the
|
||||
"Licensor." The text of the Creative Commons public licenses is dedicated to the public domain under the{" "}
|
||||
<Link href="https://creativecommons.org/publicdomain/zero/1.0/legalcode">
|
||||
<em>CC0 Public Domain Dedication</em>
|
||||
</Link>
|
||||
. Except for the limited purpose of indicating that material is shared under a Creative Commons public
|
||||
license or as otherwise permitted by the Creative Commons policies published at{" "}
|
||||
<Link href="https://creativecommons.org/policies">creativecommons.org/policies</Link>, Creative Commons does
|
||||
not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons
|
||||
without its prior written consent including, without limitation, in connection with any unauthorized
|
||||
modifications to any of its public licenses or any other arrangements, understandings, or agreements
|
||||
concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
</p>
|
||||
<p>
|
||||
Creative Commons may be contacted at <Link href="https://creativecommons.org/">creativecommons.org</Link>.
|
||||
</p>
|
||||
</Blockquote>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
42
app/manifest.ts
Normal file
42
app/manifest.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import config from "../lib/config";
|
||||
import { chrome512Png, chrome192Png, maskable512Png, maskable192Png } from "../lib/config/favicons";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const manifest = (): MetadataRoute.Manifest => {
|
||||
return {
|
||||
name: config.siteName,
|
||||
short_name: config.siteDomain,
|
||||
description: config.longDescription,
|
||||
lang: config.siteLocale,
|
||||
icons: [
|
||||
{
|
||||
src: chrome512Png.src,
|
||||
sizes: `${chrome512Png.width}x${chrome512Png.height}`,
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: chrome192Png.src,
|
||||
sizes: `${chrome192Png.width}x${chrome192Png.height}`,
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: maskable512Png.src,
|
||||
sizes: `${maskable512Png.width}x${maskable512Png.height}`,
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: maskable192Png.src,
|
||||
sizes: `${maskable192Png.width}x${maskable192Png.height}`,
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
display: "browser",
|
||||
start_url: "/",
|
||||
};
|
||||
};
|
||||
|
||||
export default manifest;
|
29
app/not-found.tsx
Normal file
29
app/not-found.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Link from "../components/Link";
|
||||
import Video from "../components/Video";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "404 Not Found",
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Video
|
||||
src={{
|
||||
webm: "/static/images/angry-panda.webm",
|
||||
mp4: "/static/images/angry-panda.mp4",
|
||||
}}
|
||||
autoplay
|
||||
responsive={false}
|
||||
style={{
|
||||
maxWidth: "400px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<h1>404: Page Not Found 😢</h1>
|
||||
|
||||
<Link href="/">Go home?</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
67
app/notes/[slug]/page.module.css
Normal file
67
app/notes/[slug]/page.module.css
Normal file
@ -0,0 +1,67 @@
|
||||
.meta {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.825em;
|
||||
line-height: 2.3;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--colors-medium);
|
||||
}
|
||||
|
||||
.meta .item {
|
||||
margin-right: 1.6em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta .link {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.meta .icon {
|
||||
display: inline;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
vertical-align: -0.2em;
|
||||
margin-right: 0.6em;
|
||||
}
|
||||
|
||||
.meta .tags {
|
||||
white-space: normal;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta .tag {
|
||||
text-transform: lowercase;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
|
||||
.meta .tag:before {
|
||||
content: "\0023"; /* cosmetically hashtagify tags */
|
||||
padding-right: 0.125em;
|
||||
color: var(--colors-light);
|
||||
}
|
||||
.meta .tag:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0.3em 0 0.5em -1px; /* misaligned left margin, super nitpicky */
|
||||
font-size: 2.1em;
|
||||
line-height: 1.3;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.title code {
|
||||
margin: 0 0.075em;
|
||||
}
|
||||
|
||||
.title .link {
|
||||
color: var(--colors-text) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
178
app/notes/[slug]/page.tsx
Normal file
178
app/notes/[slug]/page.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import * as runtime from "react/jsx-runtime";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { evaluate } from "@mdx-js/mdx";
|
||||
import Content from "../../../components/Content";
|
||||
import Link from "../../../components/Link";
|
||||
import Time from "../../../components/Time";
|
||||
import HitCounter from "../../../components/HitCounter";
|
||||
import Comments from "../../../components/Comments";
|
||||
import { getPostSlugs, getPostData } from "../../../lib/helpers/posts";
|
||||
import * as mdxComponents from "../../../lib/helpers/mdx-components";
|
||||
import { metadata as defaultMetadata } from "../../layout";
|
||||
import config from "../../../lib/config";
|
||||
import { FiCalendar, FiTag, FiEdit, FiEye } from "react-icons/fi";
|
||||
import type { Metadata, Route } from "next";
|
||||
import type { Article, WithContext } 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;
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const slugs = await getPostSlugs();
|
||||
|
||||
// map slugs into a static paths object required by next.js
|
||||
return slugs.map((slug: string) => ({
|
||||
slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const { frontMatter } = await getPostData(slug);
|
||||
|
||||
return {
|
||||
title: frontMatter.title,
|
||||
description: frontMatter.description,
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: frontMatter.title,
|
||||
url: `/notes/${slug}`,
|
||||
type: "article",
|
||||
authors: [config.authorName],
|
||||
tags: frontMatter.tags,
|
||||
publishedTime: frontMatter.date,
|
||||
modifiedTime: frontMatter.date,
|
||||
images: frontMatter.image
|
||||
? [{ url: frontMatter.image, alt: frontMatter.title }]
|
||||
: defaultMetadata.openGraph?.images,
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: `/notes/${slug}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const { frontMatter, markdown } = await getPostData(slug);
|
||||
|
||||
const jsonLd: WithContext<Article> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
name: frontMatter.title,
|
||||
description: frontMatter.description || config.longDescription,
|
||||
url: frontMatter.permalink,
|
||||
image: frontMatter.image,
|
||||
datePublished: frontMatter.date,
|
||||
dateModified: frontMatter.date,
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: config.authorName,
|
||||
url: defaultMetadata.metadataBase?.href || `https://${config.siteDomain}`,
|
||||
},
|
||||
};
|
||||
|
||||
const { remarkGfm, remarkSmartypants, rehypeSlug, rehypeUnwrapImages, rehypePrism } = await import(
|
||||
"../../../lib/helpers/remark-rehype-plugins"
|
||||
);
|
||||
|
||||
const { default: MDXContent } = await evaluate(markdown, {
|
||||
...runtime,
|
||||
remarkPlugins: [
|
||||
[remarkGfm, { singleTilde: false }],
|
||||
[
|
||||
remarkSmartypants,
|
||||
{
|
||||
quotes: true,
|
||||
dashes: "oldschool",
|
||||
backticks: false,
|
||||
ellipses: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
rehypePlugins: [rehypeSlug, rehypeUnwrapImages, [rehypePrism, { ignoreMissing: true }]],
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||
|
||||
<div className={styles.meta}>
|
||||
<div className={styles.item}>
|
||||
<Link href={`/notes/${frontMatter.slug}` as Route} underline={false} className={styles.link}>
|
||||
<FiCalendar className={styles.icon} />
|
||||
<Time date={frontMatter.date} format="MMMM D, YYYY" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{frontMatter.tags && (
|
||||
<div className={styles.item}>
|
||||
<FiTag className={styles.icon} />
|
||||
<span className={styles.tags}>
|
||||
{frontMatter.tags.map((tag) => (
|
||||
<span key={tag} title={tag} className={styles.tag} aria-label={`Tagged with ${tag}`}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.item}>
|
||||
<Link
|
||||
href={`https://github.com/${config.githubRepo}/blob/main/notes/${frontMatter.slug}.mdx`}
|
||||
title={`Edit "${frontMatter.title}" on GitHub`}
|
||||
underline={false}
|
||||
className={styles.link}
|
||||
>
|
||||
<FiEdit className={styles.icon} />
|
||||
<span>Improve This Post</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* only count hits on production site */}
|
||||
{process.env.NEXT_PUBLIC_VERCEL_ENV === "production" && (
|
||||
<div
|
||||
className={styles.item}
|
||||
style={{
|
||||
// fix potential layout shift when number of hits loads
|
||||
minWidth: "7em",
|
||||
marginRight: 0,
|
||||
}}
|
||||
>
|
||||
{/* completely hide this block if anything goes wrong on the backend */}
|
||||
<ErrorBoundary fallback={null}>
|
||||
<FiEye className={styles.icon} />
|
||||
<HitCounter slug={`notes/${frontMatter.slug}`} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className={styles.title}>
|
||||
<Link
|
||||
href={`/notes/${frontMatter.slug}` as Route}
|
||||
dangerouslySetInnerHTML={{ __html: frontMatter.htmlTitle || frontMatter.title }}
|
||||
underline={false}
|
||||
className={styles.link}
|
||||
/>
|
||||
</h1>
|
||||
|
||||
<Content>
|
||||
<MDXContent
|
||||
// @ts-ignore
|
||||
components={{ ...mdxComponents }}
|
||||
/>
|
||||
</Content>
|
||||
|
||||
{!frontMatter.noComments && (
|
||||
<div id="comments">
|
||||
<Comments title={frontMatter.title} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
51
app/notes/page.module.css
Normal file
51
app/notes/page.module.css
Normal file
@ -0,0 +1,51 @@
|
||||
.section {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.1;
|
||||
margin: 2.4em 0;
|
||||
}
|
||||
|
||||
.section:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
.section:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.year {
|
||||
font-size: 2.2em;
|
||||
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;
|
||||
}
|
||||
|
||||
.postDate {
|
||||
width: 5.25em;
|
||||
flex-shrink: 0;
|
||||
color: var(--colors-medium);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section {
|
||||
margin: 1.8em 0;
|
||||
}
|
||||
|
||||
.year {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
65
app/notes/page.tsx
Normal file
65
app/notes/page.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import Content from "../../components/Content";
|
||||
import Link from "../../components/Link";
|
||||
import Time from "../../components/Time";
|
||||
import { getAllPosts } from "../../lib/helpers/posts";
|
||||
import config from "../../lib/config";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { ReactElement } from "react";
|
||||
import type { Metadata, Route } from "next";
|
||||
import type { PostsByYear } from "../../types";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Notes",
|
||||
description: `Recent posts by ${config.authorName}.`,
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "Notes",
|
||||
url: "/notes",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/notes",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
// parse the year of each note and group them together
|
||||
const notes = await getAllPosts();
|
||||
const notesByYear: PostsByYear = {};
|
||||
|
||||
notes.forEach((note) => {
|
||||
const year = new Date(note.date).getUTCFullYear();
|
||||
(notesByYear[year] || (notesByYear[year] = [])).push(note);
|
||||
});
|
||||
|
||||
const sections: ReactElement[] = [];
|
||||
|
||||
Object.entries(notesByYear).forEach(([year, posts]) => {
|
||||
sections.push(
|
||||
<section className={styles.section} key={year}>
|
||||
<h2 className={styles.year}>{year}</h2>
|
||||
<ul className={styles.list}>
|
||||
{posts.map(({ slug, date, title, htmlTitle }) => (
|
||||
<li className={styles.post} key={slug}>
|
||||
<Time date={date} format="MMM D" className={styles.postDate} />
|
||||
<span>
|
||||
<Link href={`/notes/${slug}` as Route} dangerouslySetInnerHTML={{ __html: htmlTitle || title }} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
||||
// grouped posts enter this component ordered chronologically -- we want reverse chronological
|
||||
const reversed = sections.reverse();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Content>{reversed}</Content>
|
||||
</>
|
||||
);
|
||||
}
|
100
app/page.module.css
Normal file
100
app/page.module.css
Normal file
@ -0,0 +1,100 @@
|
||||
.page h1 {
|
||||
margin: 0 0 0.5em -1px; /* misaligned left margin, super nitpicky */
|
||||
font-size: 1.75em;
|
||||
font-weight: 500;
|
||||
line-height: 1.1;
|
||||
color: var(--colors-text);
|
||||
}
|
||||
|
||||
.page h2 {
|
||||
margin: 0.5em 0 0.5em -1px;
|
||||
font-size: 1.2em;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: var(--colors-text);
|
||||
}
|
||||
|
||||
.page p {
|
||||
margin: 0.85em 0;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.7;
|
||||
color: var(--colors-text);
|
||||
}
|
||||
|
||||
.page p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page sup {
|
||||
margin: 0 0.1em;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.pgpIcon {
|
||||
vertical-align: -0.25em;
|
||||
stroke-width: 0.5;
|
||||
}
|
||||
|
||||
.pgpKey {
|
||||
margin: 0 0.15em;
|
||||
font-family: var(--fonts-mono);
|
||||
letter-spacing: 0.075em;
|
||||
word-spacing: -0.4em;
|
||||
}
|
||||
|
||||
.wave {
|
||||
display: inline-block;
|
||||
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%;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@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) {
|
||||
.h1 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
font-size: 0.925em;
|
||||
line-height: 1.825;
|
||||
}
|
||||
}
|
294
app/page.tsx
Normal file
294
app/page.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
import { useId } from "react";
|
||||
import { GoLock } from "react-icons/go";
|
||||
import { rgba } from "polished";
|
||||
import Link from "../components/Link";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import type { Route } from "next";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
const ColorfulLink = ({
|
||||
lightColor,
|
||||
darkColor,
|
||||
children,
|
||||
...rest
|
||||
}: ComponentPropsWithoutRef<typeof Link> & {
|
||||
lightColor: string;
|
||||
darkColor: string;
|
||||
}) => {
|
||||
const uniqueId = `Link_themed__${useId().replace(/\W/g, "")}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link id={uniqueId} {...rest}>
|
||||
{children}
|
||||
</Link>
|
||||
|
||||
<style>{`.${styles.page} #${uniqueId}{color:${lightColor};--colors-linkUnderline:${rgba(lightColor, 0.4)}}[data-theme="dark"] .${styles.page} #${uniqueId}{color:${darkColor};--colors-linkUnderline:${rgba(darkColor, 0.4)}}`}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<h1>
|
||||
Hi there! I'm Jake. <span className={styles.wave}>👋</span>
|
||||
</h1>
|
||||
|
||||
<h2>
|
||||
I'm a frontend web developer based in the{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&iv_load_policy=3"
|
||||
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
|
||||
lightColor="#fb4d42"
|
||||
darkColor="#ff5146"
|
||||
>
|
||||
Boston
|
||||
</ColorfulLink>{" "}
|
||||
area.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
I specialize in{" "}
|
||||
<ColorfulLink
|
||||
href="https://reactjs.org/"
|
||||
title="React Official Website"
|
||||
lightColor="#1091b3"
|
||||
darkColor="#6fcbe3"
|
||||
>
|
||||
React
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
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"
|
||||
>
|
||||
vanilla JavaScript
|
||||
</ColorfulLink>{" "}
|
||||
to make nifty{" "}
|
||||
<ColorfulLink href="https://jamstack.wtf/" title="WTF is Jamstack?" lightColor="#04a699" darkColor="#08bbac">
|
||||
Jamstack sites
|
||||
</ColorfulLink>{" "}
|
||||
with dynamic{" "}
|
||||
<ColorfulLink
|
||||
href="https://nodejs.org/en/"
|
||||
title="Node.js Official Website"
|
||||
lightColor="#6fbc4e"
|
||||
darkColor="#84d95f"
|
||||
>
|
||||
Node.js
|
||||
</ColorfulLink>{" "}
|
||||
services. But I still know my way around less buzzwordy stacks like{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.jetbrains.com/lp/php-25/"
|
||||
title="25 Years of PHP History"
|
||||
lightColor="#8892bf"
|
||||
darkColor="#a4afe3"
|
||||
>
|
||||
LAMP
|
||||
</ColorfulLink>
|
||||
, too.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Whenever possible, I also apply my experience in{" "}
|
||||
<ColorfulLink
|
||||
href="https://bugcrowd.com/jakejarvis"
|
||||
title="Jake Jarvis on Bugcrowd"
|
||||
lightColor="#00b81a"
|
||||
darkColor="#57f06d"
|
||||
>
|
||||
application security
|
||||
</ColorfulLink>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
|
||||
title='"What is serverless computing?" on Cloudflare'
|
||||
lightColor="#0098ec"
|
||||
darkColor="#43b9fb"
|
||||
>
|
||||
serverless stacks
|
||||
</ColorfulLink>
|
||||
, and{" "}
|
||||
<ColorfulLink
|
||||
href="https://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"
|
||||
>
|
||||
DevOps automation
|
||||
</ColorfulLink>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
I fell in love with{" "}
|
||||
<ColorfulLink
|
||||
href="/previously"
|
||||
title="My Terrible, Horrible, No Good, Very Bad First Websites"
|
||||
lightColor="#4169e1"
|
||||
darkColor="#8ca9ff"
|
||||
>
|
||||
frontend web design
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
href={"/notes/my-first-code" as Route}
|
||||
title="Jake's Bulletin Board, circa 2003"
|
||||
lightColor="#9932cc"
|
||||
darkColor="#d588fb"
|
||||
>
|
||||
backend programming
|
||||
</ColorfulLink>{" "}
|
||||
when my only source of income was{" "}
|
||||
<ColorfulLink
|
||||
href="/birthday"
|
||||
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
||||
lightColor="#e40088"
|
||||
darkColor="#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
|
||||
</ColorfulLink>
|
||||
. <span style={{ color: "var(--colors-mediumLight)" }}>I've improved a bit since then, I think? 🤷</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Over the years, some of my side projects{" "}
|
||||
<ColorfulLink
|
||||
href="/leo"
|
||||
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
|
||||
lightColor="#ff1b1b"
|
||||
darkColor="#f06060"
|
||||
>
|
||||
have
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
|
||||
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
|
||||
lightColor="#f78200"
|
||||
darkColor="#fd992a"
|
||||
>
|
||||
been
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
|
||||
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
|
||||
lightColor="#f2b702"
|
||||
darkColor="#ffcc2e"
|
||||
>
|
||||
featured
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
|
||||
title='"The new Facebook is on a roll" on CNN Money'
|
||||
lightColor="#5ebd3e"
|
||||
darkColor="#78df55"
|
||||
>
|
||||
by
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.wired.com/2007/04/our-web-servers/"
|
||||
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
|
||||
lightColor="#009cdf"
|
||||
darkColor="#29bfff"
|
||||
>
|
||||
various
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
|
||||
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
|
||||
lightColor="#3e49bb"
|
||||
darkColor="#7b87ff"
|
||||
>
|
||||
media
|
||||
</ColorfulLink>{" "}
|
||||
<ColorfulLink
|
||||
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
|
||||
title='"Your Next Client? The CEO's Son" on Advertising Age'
|
||||
lightColor="#973999"
|
||||
darkColor="#db60dd"
|
||||
>
|
||||
outlets
|
||||
</ColorfulLink>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can find my work on{" "}
|
||||
<ColorfulLink
|
||||
href="https://github.com/jakejarvis"
|
||||
rel="me"
|
||||
title="Jake Jarvis on GitHub"
|
||||
lightColor="#8d4eff"
|
||||
darkColor="#a379f0"
|
||||
>
|
||||
GitHub
|
||||
</ColorfulLink>{" "}
|
||||
and{" "}
|
||||
<ColorfulLink
|
||||
href="https://www.linkedin.com/in/jakejarvis/"
|
||||
rel="me"
|
||||
title="Jake Jarvis on LinkedIn"
|
||||
lightColor="#0073b1"
|
||||
darkColor="#3b9dd2"
|
||||
>
|
||||
LinkedIn
|
||||
</ColorfulLink>
|
||||
. I'm always available to connect over{" "}
|
||||
<ColorfulLink href="/contact" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
|
||||
email
|
||||
</ColorfulLink>{" "}
|
||||
<sup>
|
||||
<ColorfulLink
|
||||
href={"/pubkey.asc" as Route}
|
||||
rel="pgpkey authn"
|
||||
title="My Public Key"
|
||||
lightColor="#757575"
|
||||
darkColor="#959595"
|
||||
underline={false}
|
||||
openInNewTab
|
||||
>
|
||||
<GoLock size="1.25em" className={styles.pgpIcon} />{" "}
|
||||
<span className={styles.pgpKey}>2B0C 9CF2 51E6 9A39</span>
|
||||
</ColorfulLink>
|
||||
</sup>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://bsky.app/profile/jarv.is"
|
||||
rel="me"
|
||||
title="Jake Jarvis on Bluesky"
|
||||
lightColor="#0085FF"
|
||||
darkColor="#208BFE"
|
||||
>
|
||||
Bluesky
|
||||
</ColorfulLink>
|
||||
,{" "}
|
||||
<ColorfulLink
|
||||
href="https://fediverse.jarv.is/@jake"
|
||||
rel="me"
|
||||
title="Jake Jarvis on Mastodon"
|
||||
lightColor="#6d6eff"
|
||||
darkColor="#7b87ff"
|
||||
>
|
||||
Mastodon
|
||||
</ColorfulLink>
|
||||
, or{" "}
|
||||
<ColorfulLink
|
||||
href="sms:+1-617-917-3737"
|
||||
title="Send SMS to +1 (617) 917-3737"
|
||||
lightColor="#6fcc01"
|
||||
darkColor="#8edb34"
|
||||
>
|
||||
SMS
|
||||
</ColorfulLink>{" "}
|
||||
as well!
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
39
app/previously/page.module.css
Normal file
39
app/previously/page.module.css
Normal file
@ -0,0 +1,39 @@
|
||||
.wackyWrapper {
|
||||
font-weight: 700;
|
||||
font-size: 1em;
|
||||
text-align: center;
|
||||
|
||||
/* classic windows 9x cursor easter egg */
|
||||
cursor:
|
||||
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAZklEQVR4AWIAgn/uBT6A9uoAAwAQiIJo97/0Rgy0ANoJH8MPeEgtqwPQEACqCoQHAKECQKgAECoAhAoAoQJAqAAQxh1oPQfcW3kJpxHtL1AAHAwEwwdYiH8BIEgBTBRAAAEEEEAAG7mRt30hEhoLAAAAAElFTkSuQmCC")
|
||||
2 1,
|
||||
auto;
|
||||
}
|
||||
|
||||
.wackyWrapper a {
|
||||
/* windows 9x hand cursor */
|
||||
cursor:
|
||||
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAgMAAAAOFJJnAAAACVBMVEVHcEwAAAD///8W1S+BAAAAAXRSTlMAQObYZgAAAEdJREFUeAFjoAVghTGkHIhghMAYmQEwxlIYYxlYlSiQMQEsELUKyli1ahWYwQZjMGIwGLKQGA4QA1EYEP0rGVAZrKGhSF4BAHw/HsVwshytAAAAAElFTkSuQmCC")
|
||||
16 12,
|
||||
auto;
|
||||
}
|
||||
|
||||
.screenshot,
|
||||
.divider {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.screenshot figcaption {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: var(--colors-medium);
|
||||
}
|
||||
|
||||
.screenshot:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 1em auto;
|
||||
}
|
220
app/previously/page.tsx
Normal file
220
app/previously/page.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
// import Layout from "../../components/Layout";
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import Figure from "../../components/Figure";
|
||||
import IFrame from "../../components/IFrame";
|
||||
import CodeInline from "../../components/CodeInline";
|
||||
import HorizontalRule from "../../components/HorizontalRule";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { ComicNeue } from "../../lib/styles/fonts";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
import img_wayback from "../../public/static/images/previously/wayback.png";
|
||||
import img_2002_02 from "../../public/static/images/previously/2002_02.png";
|
||||
import img_2002_10 from "../../public/static/images/previously/2002_10.png";
|
||||
import img_2003_08 from "../../public/static/images/previously/2003_08.png";
|
||||
import img_2004_11 from "../../public/static/images/previously/2004_11.png";
|
||||
import img_2006_04 from "../../public/static/images/previously/2006_04.png";
|
||||
import img_2006_05 from "../../public/static/images/previously/2006_05.png";
|
||||
import img_2007_01 from "../../public/static/images/previously/2007_01.png";
|
||||
import img_2007_04 from "../../public/static/images/previously/2007_04.png";
|
||||
import img_2007_05 from "../../public/static/images/previously/2007_05.png";
|
||||
import img_2009_07 from "../../public/static/images/previously/2009_07.png";
|
||||
import img_2012_09 from "../../public/static/images/previously/2012_09.png";
|
||||
import img_2018_04 from "../../public/static/images/previously/2018_04.png";
|
||||
import img_2020_03 from "../../public/static/images/previously/2020_03.png";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Previously on...",
|
||||
description: "An incredibly embarrassing and somewhat painful trip down this site's memory lane...",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "Previously on...",
|
||||
url: "/previously",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/previously",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>🕰️ Previously on...</PageTitle>
|
||||
|
||||
<Content
|
||||
className={styles.wackyWrapper}
|
||||
style={{
|
||||
fontFamily: `${ComicNeue.style.fontFamily}, var(--fonts-sans)`,
|
||||
}}
|
||||
>
|
||||
<Figure
|
||||
src={img_wayback}
|
||||
href="https://web.archive.org/web/20010501000000*/jakejarvis.com"
|
||||
alt="Timeline of this website's past."
|
||||
priority
|
||||
className={styles.screenshot}
|
||||
>
|
||||
...the{" "}
|
||||
<Link href="https://web.archive.org/web/20010501000000*/jakejarvis.com">Cringey Chronicles™</Link> of
|
||||
this website's past.
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<p style={{ marginBottom: "0.5em" }}>
|
||||
🚨 Trigger warning: excessive marquees, animated GIFs, Comic Sans, popups,{" "}
|
||||
<CodeInline
|
||||
style={{
|
||||
fontSize: "0.8em",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
color: <span style={{ color: "#32cd32" }}>limegreen</span>
|
||||
</CodeInline>{" "}
|
||||
ahead...
|
||||
</p>
|
||||
|
||||
<p style={{ fontSize: "0.95em", marginBottom: "0.5em" }}>
|
||||
<Link href="/y2k">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 24 24"
|
||||
role="img"
|
||||
style={{
|
||||
display: "inline",
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
verticalAlign: "-0.15em",
|
||||
marginRight: "0.1em",
|
||||
fill: "currentColor",
|
||||
stroke: "currentcolor",
|
||||
strokeWidth: 0,
|
||||
}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"></path>
|
||||
</svg>{" "}
|
||||
Click here for the <em>full</em> experience anyway.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<figure className={styles.screenshot}>
|
||||
<IFrame
|
||||
src="https://jakejarvis.github.io/my-first-website/"
|
||||
title="My Terrible, Horrible, No Good, Very Bad First Website"
|
||||
height={500}
|
||||
allowScripts
|
||||
style={{ margin: "0.6em 0" }}
|
||||
/>
|
||||
<figcaption>
|
||||
<Link href="https://jakejarvis.github.io/my-first-website/">November 2001</Link> (
|
||||
<Link href="https://github.com/jakejarvis/my-first-website">view source</Link>)
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2002_02} className={styles.screenshot}>
|
||||
February 2002
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2002_10} className={styles.screenshot}>
|
||||
October 2002
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2003_08} className={styles.screenshot}>
|
||||
August 2003
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2004_11} className={styles.screenshot}>
|
||||
November 2004
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2006_04} className={styles.screenshot}>
|
||||
April 2006
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2006_05} className={styles.screenshot}>
|
||||
May 2006
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2007_01} className={styles.screenshot}>
|
||||
January 2007
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2007_04} className={styles.screenshot}>
|
||||
April 2007
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2007_05} className={styles.screenshot}>
|
||||
May 2007
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure src={img_2009_07} className={styles.screenshot}>
|
||||
July 2009
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure
|
||||
src={img_2012_09}
|
||||
href="https://focused-knuth-7bc10d.netlify.app/"
|
||||
alt="September 2012"
|
||||
className={styles.screenshot}
|
||||
>
|
||||
<Link href="https://focused-knuth-7bc10d.netlify.app/">September 2012</Link> (
|
||||
<Link href="https://github.com/jakejarvis/jarv.is/tree/v1">view source</Link>)
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure
|
||||
src={img_2018_04}
|
||||
href="https://hungry-mayer-40e790.netlify.app/"
|
||||
alt="April 2018"
|
||||
className={styles.screenshot}
|
||||
>
|
||||
<Link href="https://hungry-mayer-40e790.netlify.app/">April 2018</Link> (
|
||||
<Link href="https://github.com/jakejarvis/jarv.is/tree/v2">view source</Link>)
|
||||
</Figure>
|
||||
|
||||
<HorizontalRule className={styles.divider} />
|
||||
|
||||
<Figure
|
||||
src={img_2020_03}
|
||||
href="https://quiet-truffle-92842d.netlify.app/"
|
||||
alt="March 2020"
|
||||
className={styles.screenshot}
|
||||
>
|
||||
<Link href="https://quiet-truffle-92842d.netlify.app/">March 2020</Link> (
|
||||
<Link href="https://github.com/jakejarvis/jarv.is-hugo">view source</Link>)
|
||||
</Figure>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
138
app/privacy/page.tsx
Normal file
138
app/privacy/page.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import Blockquote from "../../components/Blockquote";
|
||||
import CodeInline from "../../components/CodeInline";
|
||||
import { H2 } from "../../components/Heading";
|
||||
import { UnorderedList, ListItem } from "../../components/List";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "Privacy",
|
||||
url: "/privacy",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/privacy",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>🕵️ Privacy</PageTitle>
|
||||
|
||||
<Content>
|
||||
<p>Okay, this is an easy one. 😉</p>
|
||||
|
||||
<H2 id="hosting">Hosting</H2>
|
||||
|
||||
<p>
|
||||
Pages and first-party assets on this website are served by{" "}
|
||||
<Link href="https://vercel.com/">
|
||||
<strong>▲ Vercel</strong>
|
||||
</Link>
|
||||
. Refer to their <Link href="https://vercel.com/legal/privacy-policy">privacy policy</Link> for more
|
||||
information.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For a likely excessive level of privacy and security, this website is also mirrored on the{" "}
|
||||
<Link href="https://www.torproject.org/">🧅 Tor network</Link> at:
|
||||
</p>
|
||||
|
||||
<Blockquote style={{ overflowWrap: "break-word" }}>
|
||||
<Link href="http://jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion">
|
||||
<strong>jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion</strong>
|
||||
</Link>
|
||||
</Blockquote>
|
||||
|
||||
<H2 id="analytics">Analytics</H2>
|
||||
|
||||
<p>
|
||||
A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e.{" "}
|
||||
<CodeInline>hits = hits + 1</CodeInline>) in a <Link href="https://turso.tech/">Turso</Link> SQLite database.
|
||||
Individual views and identifying (or non-identifying) details are <strong>never stored or logged</strong>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The <Link href="https://github.com/jakejarvis/jarv.is/blob/main/prisma/schema.prisma">database schema</Link>,{" "}
|
||||
<Link href="https://github.com/jakejarvis/jarv.is/blob/main/pages/api/count.ts">serverless function</Link> and{" "}
|
||||
<Link href="https://github.com/jakejarvis/jarv.is/blob/main/components/HitCounter/HitCounter.tsx">
|
||||
client script
|
||||
</Link>{" "}
|
||||
are open source, and <Link href="https://github.com/jakejarvis/website-stats">snapshots of the database</Link>{" "}
|
||||
are public.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Link href="https://vercel.com/products/observability">
|
||||
<strong>Vercel Analytics</strong>
|
||||
</Link>{" "}
|
||||
is also used to gain insights into referrers, search terms, etc.{" "}
|
||||
<Link href="https://vercel.com/docs/analytics/privacy-policy#data-point-information">
|
||||
without collecting anything identifiable
|
||||
</Link>{" "}
|
||||
about you.
|
||||
</p>
|
||||
|
||||
<H2 id="third-party">Third-Party Content</H2>
|
||||
|
||||
<p>
|
||||
Occasionally, embedded content from third-party services is included in posts, and some may contain tracking
|
||||
code that is outside of my control. Please refer to their privacy policies for more information:
|
||||
</p>
|
||||
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://blog.codepen.io/documentation/privacy/">CodePen</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.facebook.com/policy.php">Facebook</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://docs.github.com/en/github/site-policy/github-privacy-statement">GitHub</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://soundcloud.com/pages/privacy">SoundCloud</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://twitter.com/en/privacy">Twitter</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://vimeo.com/privacy">Vimeo</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://policies.google.com/privacy">YouTube</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="spam">Fighting Spam</H2>
|
||||
|
||||
<p>
|
||||
Using{" "}
|
||||
<Link href="https://www.cloudflare.com/products/turnstile/">
|
||||
<strong>Cloudflare Turnstile</strong>
|
||||
</Link>{" "}
|
||||
to fight bot spam on the <Link href="/contact">contact form</Link> was an easy choice over seemingly
|
||||
unavoidable alternatives like <Link href="https://developers.google.com/recaptcha/">reCAPTCHA</Link>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can refer to Cloudflare's <Link href="https://www.cloudflare.com/privacypolicy/">privacy policy</Link> and{" "}
|
||||
<Link href="https://www.cloudflare.com/website-terms/">terms of service</Link> for more details. While some
|
||||
information is sent to the Turnstile API about your behavior <strong>(on the contact page only)</strong>, at
|
||||
least you won't be helping a certain internet conglomerate{" "}
|
||||
<Link href="https://blog.cloudflare.com/moving-from-recaptcha-to-hcaptcha/">
|
||||
train their self-driving cars
|
||||
</Link>
|
||||
. 🚗
|
||||
</p>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
86
app/projects/page.module.css
Normal file
86
app/projects/page.module.css
Normal file
@ -0,0 +1,86 @@
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
margin: 0.6em;
|
||||
width: 370px;
|
||||
padding: 1.2em 1.2em 0.8em 1.2em;
|
||||
border: 1px solid var(--colors-kindaLight);
|
||||
border-radius: var(--radii-corner);
|
||||
font-size: 0.85em;
|
||||
color: var(--colors-mediumDark);
|
||||
transition: border var(--transitions-fade);
|
||||
}
|
||||
|
||||
.card .name {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card .description {
|
||||
margin-top: 0.7em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.card .meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.card .metaItem {
|
||||
margin-right: 1.5em;
|
||||
line-height: 2;
|
||||
color: var(--colors-medium);
|
||||
}
|
||||
|
||||
.card .metaLink {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.card .metaLink:hover,
|
||||
.card .metaLink:focus-visible {
|
||||
color: var(--colors-link) !important;
|
||||
}
|
||||
|
||||
.card .metaIcon {
|
||||
display: inline;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: -0.3em;
|
||||
margin-right: 0.5em;
|
||||
stroke-width: 0.75;
|
||||
}
|
||||
|
||||
.card .metaLanguage {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.15em;
|
||||
height: 1.15em;
|
||||
margin-right: 0.5em;
|
||||
border-radius: 50%;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.viewMore {
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
font-weight: 500px;
|
||||
}
|
||||
|
||||
.githubIcon {
|
||||
display: inline;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
vertical-align: -0.2em;
|
||||
margin: 0 0.15em;
|
||||
fill: var(--colors-text);
|
||||
}
|
175
app/projects/page.tsx
Normal file
175
app/projects/page.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { graphql } from "@octokit/graphql";
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import RelativeTime from "../../components/RelativeTime";
|
||||
import commaNumber from "comma-number";
|
||||
import config from "../../lib/config";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import { GoStar, GoRepoForked } from "react-icons/go";
|
||||
import { SiGithub } from "react-icons/si";
|
||||
import type { Metadata } from "next";
|
||||
import type { User, Repository } from "@octokit/graphql-schema";
|
||||
import type { Project } from "../../types";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export const revalidate = 600; // 10 minutes
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Projects",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "Projects",
|
||||
url: "/projects",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/projects",
|
||||
},
|
||||
};
|
||||
|
||||
async function getRepos(): Promise<Project[] | null> {
|
||||
// don't fail the entire site build if the required API key for this page is missing
|
||||
if (!process.env.GH_PUBLIC_TOKEN || process.env.GH_PUBLIC_TOKEN === "") {
|
||||
console.warn(`ERROR: I can't fetch any GitHub projects without "GH_PUBLIC_TOKEN" set! Skipping for now...`);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/objects#repository
|
||||
const { user } = await graphql<{ user: User }>(
|
||||
`
|
||||
query ($username: String!, $sort: RepositoryOrderField!, $limit: Int) {
|
||||
user(login: $username) {
|
||||
repositories(
|
||||
first: $limit
|
||||
isLocked: false
|
||||
isFork: false
|
||||
ownerAffiliations: OWNER
|
||||
privacy: PUBLIC
|
||||
orderBy: { field: $sort, direction: DESC }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
url
|
||||
description
|
||||
pushedAt
|
||||
stargazerCount
|
||||
forkCount
|
||||
primaryLanguage {
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
username: config.authorSocial.github,
|
||||
sort: "STARGAZERS",
|
||||
limit: 12,
|
||||
headers: {
|
||||
accept: "application/vnd.github.v3+json",
|
||||
authorization: `token ${process.env.GH_PUBLIC_TOKEN}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const results = user.repositories.edges as Array<{ node: Repository }>;
|
||||
|
||||
const repos = results.map<Project>(({ node: repo }) => ({
|
||||
name: repo.name,
|
||||
url: repo.url,
|
||||
description: repo.description as string,
|
||||
updatedAt: repo.pushedAt,
|
||||
stars: repo.stargazerCount,
|
||||
forks: repo.forkCount,
|
||||
language: repo.primaryLanguage as Project["language"],
|
||||
}));
|
||||
|
||||
return repos;
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const repos = await getRepos();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle>💾 Projects</PageTitle>
|
||||
|
||||
<Content>
|
||||
<div className={styles.grid}>
|
||||
{repos?.map((repo) => (
|
||||
<div key={repo.name} className={styles.card}>
|
||||
<Link
|
||||
// @ts-ignore
|
||||
href={repo.url}
|
||||
className={styles.name}
|
||||
>
|
||||
{repo.name}
|
||||
</Link>
|
||||
|
||||
{repo.description && <p className={styles.description}>{repo.description}</p>}
|
||||
|
||||
<div className={styles.meta}>
|
||||
{repo.language && (
|
||||
<div className={styles.metaItem}>
|
||||
{repo.language.color && (
|
||||
<span className={styles.metaLanguage} style={{ backgroundColor: repo.language.color }} />
|
||||
)}
|
||||
{repo.language.name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{repo.stars && repo.stars > 0 && (
|
||||
<div className={styles.metaItem}>
|
||||
<Link
|
||||
// @ts-ignore
|
||||
href={`${repo.url}/stargazers`}
|
||||
title={`${commaNumber(repo.stars)} ${repo.stars === 1 ? "star" : "stars"}`}
|
||||
underline={false}
|
||||
className={styles.metaLink}
|
||||
>
|
||||
<GoStar className={styles.metaIcon} />
|
||||
{commaNumber(repo.stars)}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{repo.forks && repo.forks > 0 && (
|
||||
<div className={styles.metaItem}>
|
||||
<Link
|
||||
// @ts-ignore
|
||||
href={`${repo.url}/network/members`}
|
||||
title={`${commaNumber(repo.forks)} ${repo.forks === 1 ? "fork" : "forks"}`}
|
||||
underline={false}
|
||||
className={styles.metaLink}
|
||||
>
|
||||
<GoRepoForked className={styles.metaIcon} />
|
||||
{commaNumber(repo.forks)}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* only use relative "time ago" on client side, since it'll be outdated via SSG and cause hydration errors */}
|
||||
<div className={styles.metaItem}>
|
||||
<RelativeTime date={repo.updatedAt} verb="Updated" staticFormat="MMM D, YYYY" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className={styles.viewMore}>
|
||||
<Link href={`https://github.com/${config.authorSocial.github}`}>
|
||||
View more on <SiGithub className={styles.githubIcon} /> GitHub...
|
||||
</Link>
|
||||
</p>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
80
app/robots.ts
Normal file
80
app/robots.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import config from "../lib/config";
|
||||
import { metadata } from "./layout";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const robots = (): MetadataRoute.Robots => {
|
||||
// I'm already _so_ over this shit...
|
||||
// https://github.com/ai-robots-txt/ai.robots.txt/blob/main/robots.txt
|
||||
const naughtySpiders = [
|
||||
"AI2Bot",
|
||||
"Ai2Bot-Dolma",
|
||||
"Amazonbot",
|
||||
"anthropic-ai",
|
||||
"Applebot",
|
||||
"Applebot-Extended",
|
||||
"Bytespider",
|
||||
"CCBot",
|
||||
"ChatGPT-User",
|
||||
"Claude-Web",
|
||||
"ClaudeBot",
|
||||
"cohere-ai",
|
||||
"cohere-training-data-crawler",
|
||||
"Crawlspace",
|
||||
"Diffbot",
|
||||
"DuckAssistBot",
|
||||
"FacebookBot",
|
||||
"FriendlyCrawler",
|
||||
"Google-Extended",
|
||||
"GoogleOther",
|
||||
"GoogleOther-Image",
|
||||
"GoogleOther-Video",
|
||||
"GPTBot",
|
||||
"iaskspider/2.0",
|
||||
"ICC-Crawler",
|
||||
"ImagesiftBot",
|
||||
"img2dataset",
|
||||
"ISSCyberRiskCrawler",
|
||||
"Kangaroo Bot",
|
||||
"Meta-ExternalAgent",
|
||||
"Meta-ExternalFetcher",
|
||||
"OAI-SearchBot",
|
||||
"omgili",
|
||||
"omgilibot",
|
||||
"PanguBot",
|
||||
"PerplexityBot",
|
||||
"PetalBot",
|
||||
"Scrapy",
|
||||
"SemrushBot-OCOB",
|
||||
"SemrushBot-SWA",
|
||||
"Sidetrade indexer bot",
|
||||
"Timpibot",
|
||||
"VelenPublicWebCrawler",
|
||||
"Webzio-Extended",
|
||||
"YouBot",
|
||||
"AhrefsBot",
|
||||
"BLEXBot",
|
||||
"DataForSeoBot",
|
||||
"magpie-crawler",
|
||||
"MJ12bot",
|
||||
"TurnitinBot",
|
||||
];
|
||||
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
// block access to staging sites
|
||||
[process.env.NEXT_PUBLIC_VERCEL_ENV === "production" ? "allow" : "disallow"]: "/",
|
||||
},
|
||||
{
|
||||
userAgent: naughtySpiders,
|
||||
disallow: "/",
|
||||
},
|
||||
],
|
||||
sitemap: new URL("sitemap.xml", metadata.metadataBase?.href || `https://${config.siteDomain}`).href,
|
||||
};
|
||||
};
|
||||
|
||||
export default robots;
|
60
app/sitemap.ts
Normal file
60
app/sitemap.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import path from "path";
|
||||
import glob from "fast-glob";
|
||||
import { getAllPosts } from "../lib/helpers/posts";
|
||||
import { metadata } from "./layout";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
|
||||
// start with manual routes
|
||||
const routes: MetadataRoute.Sitemap = [
|
||||
{
|
||||
// homepage
|
||||
url: "/",
|
||||
priority: 1.0,
|
||||
changeFrequency: "weekly",
|
||||
lastModified: new Date(process.env.RELEASE_DATE || Date.now()), // timestamp frozen when a new build is deployed
|
||||
},
|
||||
{
|
||||
url: "/tweets/",
|
||||
changeFrequency: "yearly",
|
||||
},
|
||||
];
|
||||
|
||||
// add each directory in the app folder as a route (excluding special routes)
|
||||
(
|
||||
await glob("*", {
|
||||
cwd: path.join(process.cwd(), "app"),
|
||||
deep: 0,
|
||||
onlyDirectories: true,
|
||||
markDirectories: true,
|
||||
ignore: [
|
||||
// don't include special routes, see: https://nextjs.org/docs/app/api-reference/file-conventions/metadata
|
||||
"api",
|
||||
"feed.atom",
|
||||
"feed.xml",
|
||||
],
|
||||
})
|
||||
).forEach((route) => {
|
||||
routes.push({
|
||||
// make all URLs absolute
|
||||
url: route,
|
||||
});
|
||||
});
|
||||
|
||||
(await getAllPosts()).forEach((post) => {
|
||||
routes.push({
|
||||
url: post.permalink,
|
||||
// pull lastModified from front matter date
|
||||
lastModified: new Date(post.date),
|
||||
});
|
||||
});
|
||||
|
||||
// make all URLs absolute
|
||||
routes.forEach((page) => (page.url = new URL(page.url, metadata.metadataBase || "").href));
|
||||
|
||||
return routes;
|
||||
};
|
||||
|
||||
export default sitemap;
|
63
app/themes.css
Normal file
63
app/themes.css
Normal file
@ -0,0 +1,63 @@
|
||||
:root {
|
||||
--colors-backgroundInner: #ffffff;
|
||||
--colors-backgroundOuter: #fcfcfc;
|
||||
--colors-backgroundHeader: rgba(252, 252, 252, 0.7);
|
||||
--colors-text: #202020;
|
||||
--colors-mediumDark: #515151;
|
||||
--colors-medium: #5e5e5e;
|
||||
--colors-mediumLight: #757575;
|
||||
--colors-light: #d2d2d2;
|
||||
--colors-kindaLight: #e3e3e3;
|
||||
--colors-superLight: #f4f4f4;
|
||||
--colors-superDuperLight: #fbfbfb;
|
||||
--colors-link: #0e6dc2;
|
||||
--colors-linkUnderline: rgba(14, 109, 194, 0.4);
|
||||
--colors-success: #44a248;
|
||||
--colors-error: #ff1b1b;
|
||||
--colors-warning: #f78200;
|
||||
--colors-codeText: #313131;
|
||||
--colors-codeBackground: #fdfdfd;
|
||||
--colors-codeComment: #656e77;
|
||||
--colors-codeKeyword: #029cb9;
|
||||
--colors-codeAttribute: #70a800;
|
||||
--colors-codeNamespace: #f92672;
|
||||
--colors-codeLiteral: #ae81ff;
|
||||
--colors-codePunctuation: #111111;
|
||||
--colors-codeVariable: #d88200;
|
||||
--colors-codeAddition: #44a248;
|
||||
--colors-codeDeletion: #ff1b1b;
|
||||
--sizes-maxLayoutWidth: 865px;
|
||||
--radii-corner: 0.6rem;
|
||||
--transitions-fade: 0.25s ease;
|
||||
--transitions-linkHover: 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--colors-backgroundInner: #1e1e1e;
|
||||
--colors-backgroundOuter: #252525;
|
||||
--colors-backgroundHeader: rgba(37, 37, 37, 0.85);
|
||||
--colors-text: #f1f1f1;
|
||||
--colors-mediumDark: #d7d7d7;
|
||||
--colors-medium: #b1b1b1;
|
||||
--colors-mediumLight: #959595;
|
||||
--colors-light: #646464;
|
||||
--colors-kindaLight: #535353;
|
||||
--colors-superLight: #272727;
|
||||
--colors-superDuperLight: #1f1f1f;
|
||||
--colors-link: #88c7ff;
|
||||
--colors-linkUnderline: rgba(136, 199, 255, 0.4);
|
||||
--colors-success: #78df55;
|
||||
--colors-error: #ff5151;
|
||||
--colors-warning: #f2b702;
|
||||
--colors-codeText: #e4e4e4;
|
||||
--colors-codeBackground: #212121;
|
||||
--colors-codeComment: #929292;
|
||||
--colors-codeKeyword: #3b9dd2;
|
||||
--colors-codeAttribute: #78df55;
|
||||
--colors-codeNamespace: #f95757;
|
||||
--colors-codeLiteral: #d588fb;
|
||||
--colors-codePunctuation: #cccccc;
|
||||
--colors-codeVariable: #fd992a;
|
||||
--colors-codeAddition: #78df55;
|
||||
--colors-codeDeletion: #ff5151;
|
||||
}
|
812
app/uses/page.tsx
Normal file
812
app/uses/page.tsx
Normal file
@ -0,0 +1,812 @@
|
||||
import Content from "../../components/Content";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import Image from "../../components/Image";
|
||||
import CodeInline from "../../components/CodeInline";
|
||||
import { H2 } from "../../components/Heading";
|
||||
import { UnorderedList, ListItem } from "../../components/List";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata, Route } from "next";
|
||||
|
||||
import desktopImg from "../../public/static/images/uses/desktop.png";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "/uses",
|
||||
description: "Things I use daily.",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "/uses",
|
||||
images: [desktopImg.src],
|
||||
url: "/uses",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/uses",
|
||||
},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle>/uses</PageTitle>
|
||||
|
||||
<Content>
|
||||
<p>
|
||||
<del>I regularly get messages asking about which tools I use to work.</del>
|
||||
</p>
|
||||
<p>
|
||||
Nobody has asked me what I use. Ever. But here's a list of my daily drivers and necessities anyways, mostly
|
||||
revolving around my Apple sheepy-ness. Inspired by <Link href="https://uses.tech/">many, many others</Link>.
|
||||
❤️
|
||||
</p>
|
||||
|
||||
<Image src={desktopImg} href={desktopImg.src as Route} alt="My mess of a desktop." priority />
|
||||
|
||||
<H2 id="hardware">
|
||||
<span style={{ marginRight: "0.45em" }}>🚘</span>
|
||||
Daily Drivers
|
||||
</H2>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://browser.geekbench.com/v6/cpu/4493541">
|
||||
<strong>MacBook Pro</strong> (14-inch)
|
||||
</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>Apple M3 Pro (12‑core CPU, 18‑core GPU) 🏎️💨</ListItem>
|
||||
<ListItem>36 GB RAM</ListItem>
|
||||
<ListItem>1 TB SSD</ListItem>
|
||||
<ListItem>Space Black 🖤</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<strong>iPhone 15 Pro</strong>
|
||||
<UnorderedList>
|
||||
<ListItem>512 GB in Natural Titanium</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://a.co/d/5ixA4kx">TORRAS Magnetic Shockproof Case</Link> (surprisingly nice, also in
|
||||
Natural Titanium)
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<strong>iPad Pro 10.5"</strong>
|
||||
<UnorderedList>
|
||||
<ListItem>256 GB in Space Gray</ListItem>
|
||||
<ListItem>Smart Keyboard & Apple Pencil</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<strong>Apple Watch Series 6</strong> (GPS)
|
||||
<UnorderedList>
|
||||
<ListItem>Aluminum – Space Gray</ListItem>
|
||||
<ListItem>40mm (I have incredibly small wrists.)</ListItem>
|
||||
<ListItem>
|
||||
Usually with the{" "}
|
||||
<Link href="https://www.apple.com/shop/product/MLL02ZM/A/42mm-midnight-blue-sport-band-s-m-m-l">
|
||||
Midnight Blue sport band
|
||||
</Link>
|
||||
, 🏳️🌈{" "}
|
||||
<Link href="https://www.apple.com/shop/product/MQ4F2AM/A/38mm-pride-edition-woven-nylon">
|
||||
Pride Edition woven nylon band
|
||||
</Link>
|
||||
, or employee 🏋️{" "}
|
||||
<Link href="https://www.macrumors.com/2018/04/03/apple-employees-rewards-challenge/">
|
||||
Close Your Rings Challenge woven nylon band
|
||||
</Link>
|
||||
.
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<strong>Google Pixel 4a</strong>
|
||||
<UnorderedList>
|
||||
<ListItem>For some recreational Android development and experimentation.</ListItem>
|
||||
<ListItem>
|
||||
Activated on <Link href="https://fi.google.com/">Google Fi</Link> (
|
||||
<Link href="https://g.co/fi/r/4X38K6">referral link</Link>) during rare trips.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Really just putting this here in a futile effort to prove I'm not a <em>complete</em> Apple sheep. 🐑
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="homelab">
|
||||
<span style={{ marginRight: "0.45em" }}>🧪</span>
|
||||
Homelab
|
||||
</H2>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://store.ui.com/us/en/collections/unifi-dream-router/products/udr">
|
||||
<strong>UniFi Dream Router</strong>
|
||||
</Link>
|
||||
, plus:
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
2x{" "}
|
||||
<Link href="https://store.ui.com/us/en/collections/unifi-switching-utility-mini/products/usw-flex-mini">
|
||||
Switch Flex Mini
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://store.ui.com/us/en/products/unifi-smart-power">SmartPower Plug</Link>{" "}
|
||||
<em>
|
||||
(<Link href="https://www.youtube.com/watch?v=iW1tHr4Y_cI">It's Comcastic!™</Link>)
|
||||
</em>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.synology.com/en-us/products/DS224+">
|
||||
<strong>Synology DiskStation DS224+</strong>
|
||||
</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
2x recertified{" "}
|
||||
<Link href="https://serverpartdeals.com/products/western-digital-ultrastar-dc-hc550-wuh721816ale6l4-0f38462-16tb-7-2k-rpm-sata-6gb-s-512e-512mb-3-5-se-manufacturer-recertified-hdd">
|
||||
16TB WD Ultrastar
|
||||
</Link>{" "}
|
||||
drives
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.plex.tv/personal-media-server/">
|
||||
<strong>Plex</strong>
|
||||
</Link>{" "}
|
||||
(installed as a package via{" "}
|
||||
<Link href="https://github.com/michealespinola/syno.plexupdate">
|
||||
<CodeInline>syno.plexupdate</CodeInline>
|
||||
</Link>{" "}
|
||||
for hardware encoding)
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
A <strong>🥧 Raspberry Pi</strong> <Link href="https://a.co/d/bmii52A">cluster</Link> with:
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
1x <Link href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/">Raspberry Pi 4B</Link>, 8GB
|
||||
RAM
|
||||
</ListItem>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
Running <Link href="https://www.home-assistant.io/">Home Assistant OS</Link> with a{" "}
|
||||
<Link href="https://www.home-assistant.io/skyconnect/">SkyConnect USB Dongle</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
<ListItem>
|
||||
2x <Link href="https://www.raspberrypi.com/products/raspberry-pi-5/">Raspberry Pi 5</Link>, 4GB RAM
|
||||
</ListItem>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
Running{" "}
|
||||
<Link href="https://github.com/jakejarvis/dotfiles/tree/main/lab">a few Docker containers</Link>,
|
||||
including:
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://nextcloud.com/">Nextcloud</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://sonarr.tv/">Sonarr</Link>, <Link href="https://radarr.video/">Radarr</Link>,{" "}
|
||||
<Link href="https://www.bazarr.media/">Bazarr</Link>,{" "}
|
||||
<Link href="https://github.com/Prowlarr/Prowlarr">Prowlarr</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.qbittorrent.org/">qBittorrent</Link> (web client)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://tautulli.com/">Tautulli</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.wireguard.com/">WireGuard</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://traefik.io/traefik/">Traefik</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.authelia.com/">Authelia</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>Full post with more details coming soon!</ListItem>
|
||||
</UnorderedList>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<strong>An overpowered custom homelab server</strong>, powered by an{" "}
|
||||
<Link href="https://www.asus.com/commercial-motherboard/q87me/">ASUS Q87M-E</Link> board,{" "}
|
||||
<Link href="https://www.intel.com/content/www/us/en/products/sku/80808/intel-core-i74790s-processor-8m-cache-up-to-4-00-ghz/specifications.html">
|
||||
i7-4790S
|
||||
</Link>
|
||||
, 32 GB of RAM, 2x recertified{" "}
|
||||
<Link href="https://serverpartdeals.com/products/hgst-ultrastar-he10-0f27612-huh721008ale604-8tb-7-2k-rpm-sata-6gb-s-512e-256mb-cache-3-5-se-manufacturer-recertified-hdd">
|
||||
16TB WD Ultrastar
|
||||
</Link>{" "}
|
||||
drives, and <Link href="https://www.proxmox.com/en/proxmox-virtual-environment/overview">Proxmox VE</Link>,
|
||||
in a cheap <Link href="https://www.thermaltakeusa.com/versa-h22.html">Thermaltake Versa H22</Link> case with
|
||||
expensive <Link href="https://noctua.at/en/nf-a12x25-pwm">Noctua 🤎</Link> fans.
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="development">
|
||||
<span style={{ marginRight: "0.45em" }}>💾</span>
|
||||
Development
|
||||
</H2>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://iterm2.com/">
|
||||
<strong>iTerm 2</strong>
|
||||
</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
My various{" "}
|
||||
<Link href="https://github.com/jakejarvis/dotfiles/blob/main/zsh/aliases.zsh">ZSH aliases</Link> and{" "}
|
||||
<Link href="https://github.com/jakejarvis/dotfiles/blob/main/zsh/functions.zsh">functions</Link> are in{" "}
|
||||
<Link href="https://github.com/jakejarvis/dotfiles">
|
||||
my <CodeInline>.dotfiles</CodeInline> repository.
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://ohmyz.sh/">Oh My ZSH</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/zsh-users/zsh-autosuggestions">zsh-autosuggestions</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/zsh-users/zsh-syntax-highlighting">zsh-syntax-highlighting</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://code.visualstudio.com/">
|
||||
<strong>Visual Studio Code</strong>
|
||||
</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/jakejarvis/dotfiles/tree/main/vscode">All of my settings.</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Themes:
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=GitHub.github-vscode-theme">
|
||||
GitHub Dark theme
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme">
|
||||
Material Icon theme
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://developer.apple.com/fonts/">SF Mono font</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Extensions:
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig">
|
||||
EditorConfig
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint">
|
||||
ESLint
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github">
|
||||
GitHub Pull Requests
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=GitHub.remotehub">
|
||||
GitHub Repositories
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost">
|
||||
Import Cost
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx">MDX</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode">
|
||||
Prettier
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack">
|
||||
Remote Development Pack
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint">
|
||||
Stylelint
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount">
|
||||
Word Count
|
||||
</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://brew.sh/">
|
||||
<strong>Homebrew</strong>
|
||||
</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/jakejarvis/dotfiles/blob/main/Brewfile">
|
||||
View my messy <CodeInline>Brewfile</CodeInline> dump
|
||||
</Link>{" "}
|
||||
with all of my installed packages.
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://developer.apple.com/xcode/">
|
||||
<strong>Xcode</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.docker.com/products/docker-desktop">
|
||||
<strong>Docker Desktop</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.parallels.com/products/desktop/">
|
||||
<strong>Parallels Desktop Pro</strong>
|
||||
</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
Switched from VMware Fusion when Parallels beat them to the punch with{" "}
|
||||
<Link href="https://www.parallels.com/blogs/parallels-desktop-m1/">M1 support</Link>.
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.sketch.com/">
|
||||
<strong>Sketch</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.adobe.com/creativecloud.html">
|
||||
<strong>Adobe Creative Cloud</strong>
|
||||
</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
Still on the $20/month{" "}
|
||||
<Link href="https://www.adobe.com/creativecloud/buy/students.html">Student Plan</Link>, somehow. 🤫 Will
|
||||
need to re-evaulate once I'm kicked off; it's hard to justify spending almost 3x that...
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://panic.com/transmit/">
|
||||
<strong>Transmit</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.sequelpro.com/">
|
||||
<del>Sequel Pro</del>
|
||||
</Link>{" "}
|
||||
→{" "}
|
||||
<Link href="https://tableplus.com/">
|
||||
<strong>TablePlus</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://robomongo.org/">
|
||||
<del>Robo 3T</del>
|
||||
</Link>{" "}
|
||||
→{" "}
|
||||
<Link href="https://tableplus.com/">
|
||||
<strong>TablePlus</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://imageoptim.com/mac">
|
||||
<strong>ImageOptim</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://sipapp.io/">
|
||||
<strong>Sip</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://localwp.com/">
|
||||
<strong>Local</strong>
|
||||
</Link>{" "}
|
||||
for WordPress development.
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="browsing">
|
||||
<span style={{ marginRight: "0.45em" }}>🌎</span>
|
||||
Browsing
|
||||
</H2>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://www.mozilla.org/en-US/firefox/new/">
|
||||
<strong>Firefox</strong>
|
||||
</Link>{" "}
|
||||
🦊
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/jakejarvis/dotfiles/blob/main/firefox/user.js">
|
||||
My default <CodeInline>user.js</CodeInline> settings.
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Extensions:
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://support.1password.com/cs/1password-classic-extension/">1Password Classic</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://addons.mozilla.org/en-US/firefox/addon/betterttv/">BetterTTV</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://addons.mozilla.org/en-US/firefox/addon/darkreader/">Dark Reader</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://addons.mozilla.org/en-US/firefox/addon/decentraleyes/">Decentraleyes</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://addons.mozilla.org/en-US/firefox/addon/mailvelope/">Mailvelope</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://addons.mozilla.org/en-US/firefox/addon/react-devtools/">
|
||||
React Developer Tools
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://addons.mozilla.org/en-US/firefox/addon/octolinker/">OctoLinker</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://addons.mozilla.org/en-US/firefox/addon/privacy-badger17/">Privacy Badger</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/">uBlock Origin</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.google.com/chrome/browser/?extra=devchannel">
|
||||
<strong>Google Chrome</strong>
|
||||
</Link>{" "}
|
||||
😈
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
For testing only! See more of <Link href="#cloud">my de-Googling efforts below</Link>.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Extensions:
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://chrome.google.com/webstore/detail/amp-validator/nmoffdblmcmgeicmolmhobpoocbbmknc?hl=en">
|
||||
AMP Validator
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk?h1=en">
|
||||
Lighthouse
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en">
|
||||
React Developer Tools
|
||||
</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="macos">
|
||||
<span style={{ marginRight: "0.45em" }}>💻</span>
|
||||
macOS
|
||||
</H2>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://1password.com/">
|
||||
<strong>1Password</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.obdev.at/products/littlesnitch/index.html">
|
||||
<strong>Little Snitch</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://bjango.com/mac/istatmenus/">
|
||||
<strong>iStat Menus</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.backblaze.com/">
|
||||
<strong>Backblaze</strong>
|
||||
</Link>{" "}
|
||||
(<Link href="https://secure.backblaze.com/r/00x84e">referral link</Link>)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/billycastelli/Silicon-Info">
|
||||
<strong>Silicon Info</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.corecode.io/macupdater/">
|
||||
<strong>MacUpdater</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://freemacsoft.net/appcleaner/">
|
||||
<strong>AppCleaner</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://gpgtools.org/">
|
||||
<strong>GPG Suite</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<del>VLC</del> →{" "}
|
||||
<Link href="https://iina.io/">
|
||||
<strong>IINA</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://transmissionbt.com/">
|
||||
<strong>Transmission</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://roaringapps.com/app/amphetamine">
|
||||
<strong>Amphetamine</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.tunabellysoftware.com/tgpro/">
|
||||
<strong>TG Pro</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://coconut-flavour.com/coconutbattery/">
|
||||
<strong>coconutBattery</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.keka.io/en/">
|
||||
<strong>Keka</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://tapbots.com/ivory/mac/">
|
||||
<strong>Ivory</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="ios">
|
||||
<span style={{ marginRight: "0.45em" }}>📱</span>
|
||||
iOS
|
||||
</H2>
|
||||
<p>I have far too many apps to count, but here the essentials that have earned a spot on my home screen:</p>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://apps.apple.com/app/id568903335">
|
||||
<strong>1Password</strong> (beta)
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://apps.apple.com/app/id323229106">
|
||||
<strong>Waze</strong> (beta)
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://apps.apple.com/app/id414834813">
|
||||
<strong>Pocket Casts</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://apps.apple.com/app/id945077360">
|
||||
<strong>Sling TV</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://apps.apple.com/app/id383457673">
|
||||
<strong>Plex</strong> (beta)
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://apps.apple.com/app/id985746746">
|
||||
<strong>Discord</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://apps.apple.com/app/id1057750338">
|
||||
<strong>UniFi</strong>
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="https://apps.apple.com/app/id1385561119">
|
||||
<strong>WiFiman</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="cloud">
|
||||
<span style={{ marginRight: "0.45em" }}>☁️</span>
|
||||
Cloud
|
||||
</H2>
|
||||
<p>
|
||||
I've been making recent efforts to <Link href="https://www.stallman.org/google.html">de-Google</Link> my life,
|
||||
with mixed results...
|
||||
</p>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<del>Gmail</del> →{" "}
|
||||
<Link href="https://www.fastmail.com/">
|
||||
<strong>Fastmail</strong>
|
||||
</Link>{" "}
|
||||
(<Link href="https://ref.fm/u20274504">referral link</Link>) &{" "}
|
||||
<Link href="https://en.wikipedia.org/wiki/Apple_Mail">
|
||||
<strong>Mail.app</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<del>Google Drive</del> → <del>Dropbox</del> →{" "}
|
||||
<Link href="https://www.icloud.com/iclouddrive">
|
||||
<strong>iCloud Drive</strong>
|
||||
</Link>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href={"/notes/dropping-dropbox/" as Route}>Read why.</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<del>Google Docs</del> →{" "}
|
||||
<Link href="https://products.office.com/en-us/mac/microsoft-office-for-mac">
|
||||
<strong>Microsoft Office</strong>
|
||||
</Link>{" "}
|
||||
(hey, it works 🤷)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<del>Google Photos</del> →{" "}
|
||||
<Link href="https://www.icloud.com/photos/">
|
||||
<strong>iCloud Photos</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<del>Google Analytics</del> →{" "}
|
||||
<Link href="https://usefathom.com/">
|
||||
<strong>Fathom Analytics</strong>
|
||||
</Link>{" "}
|
||||
(<Link href="https://usefathom.com/ref/ZEYG0O">referral link</Link>)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<del>Google Public DNS</del> →{" "}
|
||||
<Link href="https://1.1.1.1/dns/">
|
||||
<strong>Cloudflare's 1.1.1.1</strong>
|
||||
</Link>{" "}
|
||||
on my home network.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<del>GoDaddy</del> → <del>Google Domains</del> →{" "}
|
||||
<Link href="https://www.cloudflare.com/products/registrar/">
|
||||
<strong>Cloudflare Registrar</strong>
|
||||
</Link>{" "}
|
||||
(and{" "}
|
||||
<Link href="https://isnic.is/en/">
|
||||
<strong>ISNIC</strong>
|
||||
</Link>{" "}
|
||||
for this domain, of course 🇮🇸)
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<p>Other geeky stuff:</p>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://vercel.com/">
|
||||
<strong>Vercel</strong>
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="https://www.netlify.com/">
|
||||
<strong>Netlify</strong>
|
||||
</Link>{" "}
|
||||
for "serverless" sites.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.digitalocean.com/">
|
||||
<strong>DigitalOcean</strong>
|
||||
</Link>{" "}
|
||||
(<Link href="https://m.do.co/c/afcf288a7dac">referral link</Link>) and{" "}
|
||||
<Link href="https://www.linode.com/">
|
||||
<strong>Linode</strong>
|
||||
</Link>{" "}
|
||||
(<Link href="https://www.linode.com/?r=0c5aeace9bd591be9fbf32f96f58470295f1ee05">referral link</Link>) for
|
||||
virtual Linux servers.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://dnsimple.com/">
|
||||
<strong>DNSimple</strong>
|
||||
</Link>{" "}
|
||||
(<Link href="https://dnsimple.com/r/eb6ced548f1e0a">referral link</Link>) and{" "}
|
||||
<Link href="https://www.cloudflare.com/">
|
||||
<strong>Cloudflare</strong>
|
||||
</Link>{" "}
|
||||
for domain DNS.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://www.backblaze.com/">
|
||||
<strong>Backblaze</strong>
|
||||
</Link>{" "}
|
||||
(<Link href="https://secure.backblaze.com/r/00x84e">referral link</Link>) for off-site Mac backups.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://gitea.io/en-us/">
|
||||
<strong>Gitea</strong>
|
||||
</Link>{" "}
|
||||
as a <Link href="https://git.jarv.is/">self-hosted</Link> Git backup/mirror.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://tailscale.com/">
|
||||
<strong>Tailscale</strong>
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="https://developers.cloudflare.com/cloudflare-one/">
|
||||
<strong>Cloudflare Zero Trust</strong>
|
||||
</Link>{" "}
|
||||
to access my home network and VPSes from anywhere.
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
|
||||
<H2 id="iot">
|
||||
<span style={{ marginRight: "0.45em" }}>🏠</span>
|
||||
Internet of <del>Things</del> <Link href={"/notes/shodan-search-queries/" as Route}>Crap</Link>
|
||||
</H2>
|
||||
<UnorderedList>
|
||||
<ListItem>
|
||||
<Link href="https://www2.meethue.com/en-us">
|
||||
<strong>Philips Hue</strong>
|
||||
</Link>{" "}
|
||||
color bulbs, dimmer switches, etc.
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
2x{" "}
|
||||
<Link href="https://www.ecobee.com/en-us/smart-thermostats/smart-wifi-thermostat/">
|
||||
<strong>ecobee3 lite</strong>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
2x{" "}
|
||||
<Link href="https://www.sonos.com/en-us/shop/one.html">
|
||||
<strong>Sonos One</strong>
|
||||
</Link>{" "}
|
||||
(with Alexa turned off...hopefully? 🤫)
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
2x{" "}
|
||||
<Link href="https://www.apple.com/apple-tv-4k/">
|
||||
<strong>Apple TV 4K</strong> (2021)
|
||||
</Link>
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
77
app/zip/page.tsx
Normal file
77
app/zip/page.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import Content from "../../components/Content";
|
||||
import Link from "../../components/Link";
|
||||
import CodeBlock from "../../components/CodeBlock/CodeBlock";
|
||||
import { metadata as defaultMetadata } from "../layout";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import backgroundImg from "../../public/static/images/zip/bg.jpg";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "fuckyougoogle.zip",
|
||||
description: "This is a horrible idea.",
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
title: "fuckyougoogle.zip",
|
||||
url: "/zip",
|
||||
},
|
||||
alternates: {
|
||||
...defaultMetadata.alternates,
|
||||
canonical: "/zip",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<Content
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImg.src})`,
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundPosition: "center",
|
||||
borderRadius: "var(--radii-corner)",
|
||||
}}
|
||||
>
|
||||
<CodeBlock
|
||||
style={{
|
||||
backgroundColor: "var(--colors-backgroundHeader)",
|
||||
backdropFilter: "saturate(180%) blur(5px))",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "var(--colors-codeNamespace)" }}>sundar</span>@
|
||||
<span style={{ color: "var(--colors-codeKeyword)" }}>google</span>:
|
||||
<span style={{ color: "var(--colors-codeAttribute)" }}>~</span>${" "}
|
||||
<span style={{ color: "var(--colors-codeLiteral)" }}>mv</span> /root
|
||||
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} underline={false}>
|
||||
/stable_products_that_people_rely_on/
|
||||
</Link>
|
||||
googledomains.zip /tmp/
|
||||
<br />
|
||||
<span style={{ color: "var(--colors-codeNamespace)" }}>sundar</span>@
|
||||
<span style={{ color: "var(--colors-codeKeyword)" }}>google</span>:
|
||||
<span style={{ color: "var(--colors-codeAttribute)" }}>~</span>${" "}
|
||||
<span style={{ color: "var(--colors-codeLiteral)" }}>crontab</span>{" "}
|
||||
<span style={{ color: "var(--colors-codeVariable)" }}>-l</span>
|
||||
<br />
|
||||
<br />
|
||||
<span style={{ color: "var(--colors-codeComment)" }}>
|
||||
# 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 bored
|
||||
& also my evil superpowers are fueled by my reckless disregard for the greater good of the internet). - xoxo
|
||||
sundar <span style={{ color: "var(--colors-codeNamespace)" }}><3</span>
|
||||
</span>
|
||||
<br />
|
||||
<span style={{ color: "var(--colors-codeAttribute)" }}>@monthly</span>
|
||||
<span style={{ color: "var(--colors-codeLiteral)" }}>rm</span>{" "}
|
||||
<span style={{ color: "var(--colors-codeVariable )" }}>-f</span> /tmp/
|
||||
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} underline={false}>
|
||||
*.zip
|
||||
</Link>
|
||||
<br />
|
||||
<br />
|
||||
<span style={{ color: "var(--colors-codeNamespace)" }}>sundar</span>@
|
||||
<span style={{ color: "var(--colors-codeKeyword)" }}>google</span>:
|
||||
<span style={{ color: "var(--colors-codeAttribute)" }}>~</span>${" "}
|
||||
<span style={{ color: "var(--colors-codeLiteral)" }}>reboot</span> 0
|
||||
</CodeBlock>
|
||||
</Content>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user