mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-07-03 15:16:40 -04:00
173
pages/_app.tsx
Normal file
173
pages/_app.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
// @ts-nocheck
|
||||
// ^ type checking causes a bunch of issues in DefaultSeo, BE CAREFUL
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Script from "next/script";
|
||||
import type { AppProps } from "next/app";
|
||||
import { DefaultSeo, SocialProfileJsonLd } from "next-seo";
|
||||
import * as config from "../lib/config";
|
||||
|
||||
import meJpg from "../public/static/images/me.jpg";
|
||||
import faviconIco from "../public/static/images/favicon.ico";
|
||||
import appleTouchIconPng from "../public/static/images/apple-touch-icon.png";
|
||||
|
||||
import "../styles/index.scss";
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// https://nextjs.org/docs/messages/next-script-for-ga
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/single-page-applications#measure_virtual_pageviews
|
||||
const handlePageview = (url: string) => {
|
||||
if (typeof window.gtag === "function") {
|
||||
window.gtag("set", "page_path", url);
|
||||
window.gtag("event", "page_view");
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on("routeChangeComplete", handlePageview);
|
||||
return () => {
|
||||
router.events.off("routeChangeComplete", handlePageview);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DefaultSeo
|
||||
defaultTitle={`${config.siteName} – ${config.shortDescription}`}
|
||||
titleTemplate={`%s – ${config.siteName}`}
|
||||
description={config.longDescription}
|
||||
canonical={`${config.baseURL}/`}
|
||||
openGraph={{
|
||||
site_name: config.siteName,
|
||||
title: `${config.siteName} – ${config.shortDescription}`,
|
||||
url: `${config.baseURL}/`,
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: `${config.baseURL}${meJpg.src}`,
|
||||
alt: `${config.siteName} – ${config.shortDescription}`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
twitter={{
|
||||
handle: `@${config.twitterHandle}`,
|
||||
site: `@${config.twitterHandle}`,
|
||||
cardType: "summary",
|
||||
}}
|
||||
facebook={{
|
||||
appId: config.facebookAppId,
|
||||
}}
|
||||
additionalLinkTags={[
|
||||
{
|
||||
rel: "icon",
|
||||
href: "data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cpath fill='%236fbc4e' d='m27.7 5 17.1 9.8 8.5-4.9-17-9.9z'/%3E%3Cpath fill='%23ffb900' d='m27.7 14.8 8.6 4.9v19.7l8.5 4.9V14.8L27.7 5zm-8.5 24.6-8.5-4.9v19.6l17 9.9v-9.8l-8.5-5z'/%3E%3Cpath fill='%23009cdf' d='M27.7 44.3v-9.8l-8.5 4.9v9.8zm17.1 0-17.1 9.9V64l25.6-14.7V9.9l-8.5 4.9z'/%3E%3Cpath fill='%236fbc4e' d='m10.7 34.5 8.5 4.9 8.5-4.9-8.5-4.9zm8.5 14.7 8.5 5 17.1-9.9-8.5-4.9z'/%3E%3C/svg%3E",
|
||||
type: "image/svg+xml",
|
||||
},
|
||||
{
|
||||
rel: "icon",
|
||||
href: faviconIco.src,
|
||||
},
|
||||
{
|
||||
rel: "apple-touch-icon",
|
||||
href: appleTouchIconPng.src,
|
||||
sizes: `${appleTouchIconPng.width}x${appleTouchIconPng.height}`,
|
||||
},
|
||||
{
|
||||
rel: "manifest",
|
||||
href: "/site.webmanifest",
|
||||
},
|
||||
{
|
||||
rel: "alternate",
|
||||
href: `/feed.xml`,
|
||||
type: "application/rss+xml",
|
||||
title: `${config.siteName} (RSS)`,
|
||||
},
|
||||
{
|
||||
rel: "alternate",
|
||||
href: `/feed.atom`,
|
||||
type: "application/atom+xml",
|
||||
title: `${config.siteName} (Atom)`,
|
||||
},
|
||||
{
|
||||
rel: "webmention",
|
||||
href: `https://webmention.io/${config.webmentionId}/webmention`,
|
||||
},
|
||||
{
|
||||
rel: "pingback",
|
||||
href: `https://webmention.io/${config.webmentionId}/xmlrpc`,
|
||||
},
|
||||
{
|
||||
rel: "humans",
|
||||
href: `/humans.txt`,
|
||||
},
|
||||
{
|
||||
rel: "pgpkey",
|
||||
href: `/pubkey.asc`,
|
||||
type: "application/pgp-keys",
|
||||
},
|
||||
]}
|
||||
additionalMetaTags={[
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
content: config.authorName,
|
||||
},
|
||||
{
|
||||
name: "monetization",
|
||||
content: config.monetization,
|
||||
},
|
||||
{
|
||||
name: "twitter:dnt",
|
||||
content: "on",
|
||||
},
|
||||
{
|
||||
name: "twitter:widgets:csp",
|
||||
content: "on",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<SocialProfileJsonLd
|
||||
type="Person"
|
||||
name="Jake Jarvis"
|
||||
url={`${config.baseURL}/`}
|
||||
sameAs={[
|
||||
`${config.baseURL}/`,
|
||||
"https://github.com/jakejarvis",
|
||||
"https://keybase.io/jakejarvis",
|
||||
"https://twitter.com/jakejarvis",
|
||||
"https://medium.com/@jakejarvis",
|
||||
"https://www.linkedin.com/in/jakejarvis/",
|
||||
"https://www.facebook.com/jakejarvis",
|
||||
"https://www.instagram.com/jakejarvis/",
|
||||
"https://mastodon.social/@jakejarvis",
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Inline script to restore light/dark theme preference ASAP */}
|
||||
<Script id="restore_theme" strategy="afterInteractive">{`
|
||||
try {
|
||||
var pref = localStorage.getItem("dark_mode"),
|
||||
dark = pref === "true" || (!pref && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
|
||||
} catch (e) {}`}</Script>
|
||||
|
||||
<Script src={`https://www.googletagmanager.com/gtag/js?id=UA-1563964-4`} strategy="afterInteractive" />
|
||||
<Script id="ga4" strategy="afterInteractive">{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-1563964-4', {
|
||||
anonymize_ip: true
|
||||
});`}</Script>
|
||||
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
}
|
15
pages/_document.tsx
Normal file
15
pages/_document.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en-us">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
112
pages/api/contact.ts
Normal file
112
pages/api/contact.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import * as Sentry from "@sentry/node";
|
||||
import fetch from "node-fetch";
|
||||
import queryString from "query-string";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN || "",
|
||||
environment: process.env.NODE_ENV || process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV || "",
|
||||
});
|
||||
|
||||
// fallback to dummy secret for testing: https://docs.hcaptcha.com/#integration-testing-test-keys
|
||||
const HCAPTCHA_SITE_KEY =
|
||||
process.env.HCAPTCHA_SITE_KEY || process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY || "10000000-ffff-ffff-ffff-000000000001";
|
||||
const HCAPTCHA_SECRET_KEY = process.env.HCAPTCHA_SECRET_KEY || "0x0000000000000000000000000000000000000000";
|
||||
const HCAPTCHA_API_ENDPOINT = "https://hcaptcha.com/siteverify";
|
||||
|
||||
const { AIRTABLE_API_KEY, AIRTABLE_BASE } = process.env;
|
||||
const AIRTABLE_API_ENDPOINT = "https://api.airtable.com/v0/";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
// permissive access control headers
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
// disable caching on both ends
|
||||
res.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Expires", 0);
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).send(""); // 405 Method Not Allowed
|
||||
}
|
||||
|
||||
const { body } = req;
|
||||
|
||||
// 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 (!body || !body.name || !body.email || !body.message) {
|
||||
// all fields are required
|
||||
throw new Error("USER_MISSING_DATA");
|
||||
}
|
||||
if (!body["h-captcha-response"] || !(await validateCaptcha(body["h-captcha-response"]))) {
|
||||
// either the captcha is wrong or completely missing
|
||||
throw new Error("USER_INVALID_CAPTCHA");
|
||||
}
|
||||
|
||||
// sent directly to airtable
|
||||
const airtableResult = await sendToAirtable({
|
||||
Name: body.name,
|
||||
Email: body.email,
|
||||
Message: body.message,
|
||||
});
|
||||
|
||||
// throw an internal error, not user's fault
|
||||
if (airtableResult !== true) {
|
||||
throw new Error("AIRTABLE_API_ERROR");
|
||||
}
|
||||
|
||||
// return in JSON format
|
||||
return res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
const message = error instanceof Error ? error.message : "UNKNOWN_EXCEPTION";
|
||||
|
||||
// don't log PEBCAK errors to sentry
|
||||
if (!message.startsWith("USER_")) {
|
||||
// log error to sentry, give it 2 seconds to finish sending
|
||||
Sentry.captureException(error);
|
||||
await Sentry.flush(2000);
|
||||
}
|
||||
|
||||
// 500 Internal Server Error
|
||||
return res.status(500).json({ success: false, message });
|
||||
}
|
||||
};
|
||||
|
||||
const validateCaptcha = async (formResponse) => {
|
||||
const response = await fetch(HCAPTCHA_API_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: queryString.stringify({
|
||||
response: formResponse,
|
||||
sitekey: HCAPTCHA_SITE_KEY,
|
||||
secret: HCAPTCHA_SECRET_KEY,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// @ts-ignore
|
||||
return result.success;
|
||||
};
|
||||
|
||||
const sendToAirtable = async (data) => {
|
||||
const response = await fetch(`${AIRTABLE_API_ENDPOINT}${AIRTABLE_BASE}/Messages`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${AIRTABLE_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fields: data,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
};
|
||||
|
||||
export default handler;
|
133
pages/api/hits.ts
Normal file
133
pages/api/hits.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import * as Sentry from "@sentry/node";
|
||||
import * as config from "../../lib/config";
|
||||
import Parser from "rss-parser";
|
||||
import { decode } from "html-entities";
|
||||
import pRetry from "p-retry";
|
||||
import faunadb from "faunadb";
|
||||
const q = faunadb.query;
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN || "",
|
||||
environment: process.env.NODE_ENV || process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV || "",
|
||||
});
|
||||
|
||||
const BASE_URL = config.baseURL === "" ? `${config.siteDomain}/` : `${config.baseURL}/`;
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
// permissive access control headers
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).send(""); // 405 Method Not Allowed
|
||||
}
|
||||
|
||||
const client = new faunadb.Client({
|
||||
secret: process.env.FAUNADB_SERVER_SECRET,
|
||||
checkNewVersion: false, // https://github.com/fauna/faunadb-js/pull/504
|
||||
});
|
||||
const { slug } = req.query;
|
||||
let result;
|
||||
|
||||
if (!slug || slug === "/") {
|
||||
// return overall site stats if slug not specified
|
||||
result = await getSiteStats(client);
|
||||
|
||||
// let Vercel edge and browser cache results for 15 mins
|
||||
res.setHeader("Cache-Control", "public, max-age=900, s-maxage=900, stale-while-revalidate");
|
||||
} else {
|
||||
// increment this page's hits. retry 3 times in case of Fauna "contended transaction" error:
|
||||
// https://sentry.io/share/issue/9c60a58211954ed7a8dfbe289bd107b5/
|
||||
result = await pRetry(() => incrementPageHits(slug, client), {
|
||||
onFailedAttempt: (error) => {
|
||||
console.warn(`Attempt ${error.attemptNumber} failed, trying again...`);
|
||||
},
|
||||
retries: 3,
|
||||
});
|
||||
|
||||
// disable caching on both ends
|
||||
res.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Expires", 0);
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
}
|
||||
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
// log error to sentry, give it 2 seconds to finish sending
|
||||
Sentry.captureException(error);
|
||||
await Sentry.flush(2000);
|
||||
|
||||
const message = error instanceof Error ? error.message : "Unknown error.";
|
||||
|
||||
// 500 Internal Server Error
|
||||
return res.status(500).json({ success: false, message });
|
||||
}
|
||||
};
|
||||
|
||||
const incrementPageHits = async (slug, client) => {
|
||||
const result = await client.query(
|
||||
q.Let(
|
||||
{ match: q.Match(q.Index("hits_by_slug"), slug) },
|
||||
q.If(
|
||||
q.Exists(q.Var("match")),
|
||||
q.Let(
|
||||
{
|
||||
ref: q.Select("ref", q.Get(q.Var("match"))),
|
||||
hits: q.ToInteger(q.Select("hits", q.Select("data", q.Get(q.Var("match"))))),
|
||||
},
|
||||
q.Update(q.Var("ref"), { data: { hits: q.Add(q.Var("hits"), 1) } })
|
||||
),
|
||||
q.Create(q.Collection("hits"), { data: { slug, hits: 1 } })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// send client the *new* hit count
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const getSiteStats = async (client) => {
|
||||
// get database and RSS results asynchronously
|
||||
const parser = new Parser();
|
||||
const [feed, result] = await Promise.all([
|
||||
parser.parseURL(`${BASE_URL}feed.xml`),
|
||||
client.query(
|
||||
q.Map(
|
||||
q.Paginate(q.Documents(q.Collection("hits")), { size: 99 }),
|
||||
q.Lambda((x) => q.Select("data", q.Get(x)))
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
const pages = result.data;
|
||||
const stats = {
|
||||
total: { hits: 0 },
|
||||
pages,
|
||||
};
|
||||
|
||||
pages.map((p) => {
|
||||
// match URLs from RSS feed with db to populate some metadata
|
||||
const match = feed.items.find((x) => x.link === `${BASE_URL}${p.slug}/`);
|
||||
if (match) {
|
||||
p.title = decode(match.title);
|
||||
p.url = match.link;
|
||||
p.date = new Date(match.pubDate);
|
||||
}
|
||||
|
||||
// add these hits to running tally
|
||||
stats.total.hits += p.hits;
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
// sort by hits (descending)
|
||||
stats.pages.sort((a, b) => (a.hits > b.hits ? -1 : 1));
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
export default handler;
|
99
pages/api/projects.ts
Normal file
99
pages/api/projects.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { graphql } from "@octokit/graphql";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN || "",
|
||||
environment: process.env.NODE_ENV || process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV || "",
|
||||
});
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
// permissive access control headers
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).send(""); // 405 Method Not Allowed
|
||||
}
|
||||
|
||||
// allow custom limit, max. 24 results
|
||||
let limit = 24;
|
||||
if (parseInt(req.query.limit as string) > 0 && parseInt(req.query.limit as string) < limit) {
|
||||
limit = parseInt(req.query.limit as string);
|
||||
}
|
||||
|
||||
const result = await fetchRepos(req.query.sort === "top" ? "STARGAZERS" : "PUSHED_AT", limit);
|
||||
|
||||
// let Vercel edge and browser cache results for 15 mins
|
||||
res.setHeader("Cache-Control", "public, max-age=900, s-maxage=900, stale-while-revalidate");
|
||||
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
// log error to sentry, give it 2 seconds to finish sending
|
||||
Sentry.captureException(error);
|
||||
await Sentry.flush(2000);
|
||||
|
||||
const message = error instanceof Error ? error.message : "Unknown error.";
|
||||
|
||||
// 500 Internal Server Error
|
||||
return res.status(500).json({ success: false, message });
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRepos = async (sort, limit) => {
|
||||
// https://docs.github.com/en/graphql/reference/objects#repository
|
||||
const { user } = await graphql(
|
||||
`
|
||||
query ($username: String!, $sort: String, $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: "jakejarvis",
|
||||
limit,
|
||||
sort,
|
||||
headers: {
|
||||
authorization: `token ${process.env.GH_PUBLIC_TOKEN}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return user.repositories.edges.map(({ node: repo }) => ({
|
||||
name: repo.name,
|
||||
url: repo.url,
|
||||
description: repo.description,
|
||||
updatedAt: new Date(repo.pushedAt),
|
||||
stars: repo.stargazerCount,
|
||||
forks: repo.forkCount,
|
||||
language: repo.primaryLanguage,
|
||||
}));
|
||||
};
|
||||
|
||||
export default handler;
|
139
pages/api/tracks.ts
Normal file
139
pages/api/tracks.ts
Normal file
@ -0,0 +1,139 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// Fetches my Spotify most-played tracks or currently playing track.
|
||||
// Heavily inspired by @leerob: https://leerob.io/snippets/spotify
|
||||
|
||||
import * as Sentry from "@sentry/node";
|
||||
import fetch from "node-fetch";
|
||||
import queryString from "query-string";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN || "",
|
||||
environment: process.env.NODE_ENV || process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV || "",
|
||||
});
|
||||
|
||||
const { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN } = process.env;
|
||||
|
||||
const basic = Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64");
|
||||
|
||||
// https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow
|
||||
const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
|
||||
// https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-the-users-currently-playing-track
|
||||
const NOW_PLAYING_ENDPOINT = "https://api.spotify.com/v1/me/player/currently-playing";
|
||||
// https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks
|
||||
const TOP_TRACKS_ENDPOINT = "https://api.spotify.com/v1/me/top/tracks?time_range=long_term&limit=10";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
// permissive access control headers
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).send(""); // 405 Method Not Allowed
|
||||
}
|
||||
|
||||
// default to top tracks
|
||||
let response;
|
||||
|
||||
// get currently playing track (/api/tracks/?now), otherwise top 10 tracks
|
||||
if (typeof req.query.now !== "undefined") {
|
||||
response = await getNowPlaying();
|
||||
|
||||
// let Vercel edge and browser cache results for 5 mins
|
||||
res.setHeader("Cache-Control", "public, max-age=300, s-maxage=300, stale-while-revalidate");
|
||||
} else {
|
||||
response = await getTopTracks();
|
||||
|
||||
// let Vercel edge and browser cache results for 3 hours
|
||||
res.setHeader("Cache-Control", "public, max-age=10800, s-maxage=10800, stale-while-revalidate");
|
||||
}
|
||||
|
||||
return res.status(200).json(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
// log error to sentry, give it 2 seconds to finish sending
|
||||
Sentry.captureException(error);
|
||||
await Sentry.flush(2000);
|
||||
|
||||
const message = error instanceof Error ? error.message : "Unknown error.";
|
||||
|
||||
// 500 Internal Server Error
|
||||
return res.status(500).json({ success: false, message });
|
||||
}
|
||||
};
|
||||
|
||||
const getAccessToken = async () => {
|
||||
const response = await fetch(TOKEN_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${basic}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: queryString.stringify({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: SPOTIFY_REFRESH_TOKEN,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const getNowPlaying = async () => {
|
||||
const { access_token } = await getAccessToken();
|
||||
|
||||
const response = await fetch(NOW_PLAYING_ENDPOINT, {
|
||||
headers: {
|
||||
// eslint-disable-next-line camelcase
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 204 || response.status > 400) {
|
||||
return { isPlaying: false };
|
||||
}
|
||||
|
||||
const active = await response.json();
|
||||
|
||||
if (active.is_playing === true && active.item) {
|
||||
return {
|
||||
isPlaying: active.is_playing,
|
||||
artist: active.item.artists.map((artist) => artist.name).join(", "),
|
||||
title: active.item.name,
|
||||
album: active.item.album.name,
|
||||
imageUrl: active.item.album.images ? active.item.album.images[0].url : undefined,
|
||||
songUrl: active.item.external_urls.spotify,
|
||||
};
|
||||
}
|
||||
|
||||
return { isPlaying: false };
|
||||
};
|
||||
|
||||
const getTopTracks = async () => {
|
||||
const { access_token } = await getAccessToken();
|
||||
|
||||
const response = await fetch(TOP_TRACKS_ENDPOINT, {
|
||||
headers: {
|
||||
// eslint-disable-next-line camelcase
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const { items } = await response.json();
|
||||
|
||||
return items.map((track) => ({
|
||||
artist: track.artists.map((artist) => artist.name).join(", "),
|
||||
title: track.name,
|
||||
album: track.album.name,
|
||||
imageUrl: track.album.images ? track.album.images[0].url : undefined,
|
||||
songUrl: track.external_urls.spotify,
|
||||
}));
|
||||
};
|
||||
|
||||
export default handler;
|
49
pages/birthday.tsx
Normal file
49
pages/birthday.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import Content from "../components/Content";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import Video from "../components/video/FullPageVideo";
|
||||
import { TapeIcon } from "../components/icons";
|
||||
|
||||
import thumbnail from "../public/static/images/birthday/thumb.png";
|
||||
|
||||
export default function Birthday() {
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Container
|
||||
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
||||
description="The origin of my hatred for the Happy Birthday song."
|
||||
>
|
||||
<PageTitle
|
||||
title={
|
||||
<>
|
||||
<TapeIcon /> 1996.MOV
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Content>
|
||||
<Video
|
||||
url={[
|
||||
{ src: "/static/images/birthday/birthday.webm", type: "video/webm" },
|
||||
{ src: "/static/images/birthday/birthday.mp4", type: "video/mp4" },
|
||||
]}
|
||||
config={{
|
||||
// @ts-ignore
|
||||
file: {
|
||||
attributes: {
|
||||
poster: thumbnail.src,
|
||||
controlsList: "nodownload",
|
||||
preload: "metadata",
|
||||
autoPlay: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
controls={true}
|
||||
/>
|
||||
</Content>
|
||||
</Container>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
94
pages/cli.tsx
Normal file
94
pages/cli.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import Image from "next/image";
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import Content from "../components/Content";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import { BotIcon } from "../components/icons";
|
||||
|
||||
import cliImg from "../public/static/images/cli/screenshot.png";
|
||||
|
||||
export default function CLI() {
|
||||
return (
|
||||
<Layout>
|
||||
<Container
|
||||
title="CLI"
|
||||
description="AKA, the most useless Node module ever published, in history, by anyone, ever."
|
||||
>
|
||||
<PageTitle
|
||||
title={
|
||||
<>
|
||||
<BotIcon /> CLI
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Content>
|
||||
<blockquote>
|
||||
<p>
|
||||
The{" "}
|
||||
<a href="https://jarv.is/" target="_blank" rel="noopener noreferrer">
|
||||
Jake Jarvis
|
||||
</a>{" "}
|
||||
CLI (aka the most useless Node module ever published, in history, by anyone, ever).
|
||||
</p>
|
||||
</blockquote>
|
||||
<a
|
||||
className="no-underline"
|
||||
href="https://www.npmjs.com/package/@jakejarvis/cli"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image src={cliImg} placeholder="blur" alt="Terminal Screenshot" />
|
||||
</a>
|
||||
<h2>Usage</h2>
|
||||
<pre>
|
||||
<code>npx @jakejarvis/cli</code>
|
||||
</pre>
|
||||
<h2>Inspired by</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/sindresorhus/sindresorhus-cli" target="_blank" rel="noopener noreferrer">
|
||||
@sindresorhus/sindresorhus-cli
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/yg/ygcodes" target="_blank" rel="noopener noreferrer">
|
||||
@yg/ygcodes
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Built with</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/vadimdemedes/ink" target="_blank" rel="noopener noreferrer">
|
||||
ink
|
||||
</a>{" "}
|
||||
- React for interactive command-line apps
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/sindresorhus/meow" target="_blank" rel="noopener noreferrer">
|
||||
meow
|
||||
</a>{" "}
|
||||
- CLI helper
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<a href="https://github.com/jakejarvis/jakejarvis/tree/main/cli" target="_blank" rel="noreferrer">
|
||||
View source on GitHub.
|
||||
</a>
|
||||
</p>
|
||||
<h2>License</h2>
|
||||
<p>
|
||||
MIT ©{" "}
|
||||
<a href="https://jarv.is/" target="_blank" rel="noopener noreferrer">
|
||||
Jake Jarvis
|
||||
</a>
|
||||
,{" "}
|
||||
<a href="https://sindresorhus.com" target="_blank" rel="noopener noreferrer">
|
||||
Sindre Sorhus
|
||||
</a>
|
||||
</p>
|
||||
</Content>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
}
|
59
pages/contact.tsx
Normal file
59
pages/contact.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import ContactForm from "../components/contact/ContactForm";
|
||||
import { MailIcon, LockIcon } from "../components/icons";
|
||||
|
||||
export default function Contact() {
|
||||
return (
|
||||
<Layout>
|
||||
<Container title="✉️ Contact Me">
|
||||
<PageTitle
|
||||
title={
|
||||
<>
|
||||
<MailIcon /> Contact Me
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<p>
|
||||
Fill out this quick form and I'll get back to you as soon as I can! You can also{" "}
|
||||
<a href="mailto:jake@jarv.is">email me directly</a>, send me a{" "}
|
||||
<a
|
||||
href="https://twitter.com/messages/compose?recipient_id=229769022"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
>
|
||||
direct message on Twitter
|
||||
</a>
|
||||
, or <a href="sms:+1-617-917-3737">text me</a>.
|
||||
</p>
|
||||
<p>
|
||||
<LockIcon /> You can grab my public key here:{" "}
|
||||
<a href="/pubkey.asc" title="My Public PGP Key" target="_blank" rel="pgpkey authn noopener">
|
||||
<code>6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</code>
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<ContactForm />
|
||||
</div>
|
||||
<style jsx>{`
|
||||
div {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
font-size: 0.925em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
code {
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
word-spacing: -0.175em;
|
||||
white-space: normal;
|
||||
}
|
||||
`}</style>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
}
|
24
pages/feed.atom.ts
Normal file
24
pages/feed.atom.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { getAllNotes } from "../lib/parseNotes";
|
||||
import { buildFeed } from "../lib/buildFeed";
|
||||
import type { GetServerSideProps } from "next";
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
if (context && context.res) {
|
||||
const { res } = context;
|
||||
|
||||
const notes = getAllNotes(["title", "date", "image", "slug", "description"]);
|
||||
|
||||
const feed = buildFeed(notes);
|
||||
res.setHeader("content-type", "application/atom+xml");
|
||||
res.write(feed.atom1());
|
||||
res.end();
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
};
|
||||
|
||||
const AtomPage = () => null;
|
||||
|
||||
export default AtomPage;
|
24
pages/feed.xml.ts
Normal file
24
pages/feed.xml.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { getAllNotes } from "../lib/parseNotes";
|
||||
import { buildFeed } from "../lib/buildFeed";
|
||||
import type { GetServerSideProps } from "next";
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
if (context && context.res) {
|
||||
const { res } = context;
|
||||
|
||||
const notes = getAllNotes(["title", "date", "image", "slug", "description"]);
|
||||
|
||||
const feed = buildFeed(notes);
|
||||
res.setHeader("content-type", "application/rss+xml");
|
||||
res.write(feed.rss2());
|
||||
res.end();
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
};
|
||||
|
||||
const RssPage = () => null;
|
||||
|
||||
export default RssPage;
|
79
pages/hillary.tsx
Normal file
79
pages/hillary.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import Content from "../components/Content";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import Video from "../components/video/FullPageVideo";
|
||||
|
||||
import thumbnail from "../public/static/images/hillary/thumb.png";
|
||||
|
||||
export default function Hillary() {
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Container
|
||||
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."
|
||||
>
|
||||
<PageTitle title="My Brief Apperance in Hillary Clinton's DNC Video" />
|
||||
<Content>
|
||||
<Video
|
||||
url={[
|
||||
{ src: "/static/images/hillary/convention-720p.webm", type: "video/webm" },
|
||||
{ src: "/static/images/hillary/convention-720p.mp4", type: "video/mp4" },
|
||||
]}
|
||||
config={{
|
||||
// @ts-ignore
|
||||
file: {
|
||||
attributes: {
|
||||
poster: thumbnail.src,
|
||||
controlsList: "nodownload",
|
||||
preload: "metadata",
|
||||
autoPlay: false,
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
kind: "subtitles",
|
||||
src: "/static/images/hillary/subs.en.vtt",
|
||||
srcLang: "en",
|
||||
label: "English",
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
controls={true}
|
||||
/>
|
||||
<p className="copyright">
|
||||
Video is property of{" "}
|
||||
<a href="https://www.hillaryclinton.com/" target="_blank" rel="noopener noreferrer">
|
||||
Hillary for America
|
||||
</a>
|
||||
, the{" "}
|
||||
<a href="https://democrats.org/" target="_blank" rel="noopener noreferrer">
|
||||
Democratic National Committee
|
||||
</a>
|
||||
, and{" "}
|
||||
<a href="https://cnnpressroom.blogs.cnn.com/" target="_blank" rel="noopener noreferrer">
|
||||
CNN / WarnerMedia
|
||||
</a>
|
||||
. © 2016.
|
||||
</p>
|
||||
<style jsx>{`
|
||||
.copyright {
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.8;
|
||||
margin: 1.25em 1em 0.5em;
|
||||
color: var(--medium-light);
|
||||
}
|
||||
|
||||
.copyright a {
|
||||
font-weight: 700;
|
||||
}
|
||||
`}</style>
|
||||
</Content>
|
||||
</Container>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
13
pages/index.tsx
Normal file
13
pages/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import Home from "../components/home/Home";
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<Layout>
|
||||
<Container>
|
||||
<Home />
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
}
|
79
pages/leo.tsx
Normal file
79
pages/leo.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import Content from "../components/Content";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import Video from "../components/video/FullPageVideo";
|
||||
|
||||
import thumbnail from "../public/static/images/leo/thumb.png";
|
||||
|
||||
export default function Leo() {
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Container
|
||||
title='Facebook App on "The Lab with Leo Laporte"'
|
||||
description="Powncer app featured in Leo Laporte's TechTV show."
|
||||
>
|
||||
<PageTitle title='Facebook App on "The Lab with Leo Laporte"' />
|
||||
<Content>
|
||||
<Video
|
||||
url={[
|
||||
{ src: "/static/images/leo/leo.webm", type: "video/webm" },
|
||||
{ src: "/static/images/leo/leo.mp4", type: "video/mp4" },
|
||||
]}
|
||||
config={{
|
||||
// @ts-ignore
|
||||
file: {
|
||||
attributes: {
|
||||
poster: thumbnail.src,
|
||||
controlsList: "nodownload",
|
||||
preload: "metadata",
|
||||
autoPlay: false,
|
||||
},
|
||||
tracks: [
|
||||
{
|
||||
kind: "subtitles",
|
||||
src: "/static/images/leo/subs.en.vtt",
|
||||
srcLang: "en",
|
||||
label: "English",
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
controls={true}
|
||||
/>
|
||||
<p className="copyright">
|
||||
Video is property of{" "}
|
||||
<a
|
||||
href="https://web.archive.org/web/20070511004304/http://www.g4techtv.ca/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
G4techTV Canada
|
||||
</a>{" "}
|
||||
&{" "}
|
||||
<a href="https://leolaporte.com/" target="_blank" rel="noopener noreferrer">
|
||||
Leo Laporte
|
||||
</a>
|
||||
. © 2007 G4 Media, Inc.
|
||||
</p>
|
||||
<style jsx>{`
|
||||
.copyright {
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.8;
|
||||
margin: 1.25em 1em 0.5em;
|
||||
color: var(--medium-light);
|
||||
}
|
||||
|
||||
.copyright a {
|
||||
font-weight: 700;
|
||||
}
|
||||
`}</style>
|
||||
</Content>
|
||||
</Container>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
475
pages/license.tsx
Normal file
475
pages/license.tsx
Normal file
@ -0,0 +1,475 @@
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import Content from "../components/Content";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import { LicenseIcon } from "../components/icons";
|
||||
|
||||
export default function License() {
|
||||
return (
|
||||
<Layout>
|
||||
<Container title="License">
|
||||
<PageTitle
|
||||
title={
|
||||
<>
|
||||
<LicenseIcon /> License
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Content>
|
||||
<p>
|
||||
Unless otherwise noted, content on this website is published under the{" "}
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">
|
||||
<strong>Creative Commons Attribution 4.0 International Public License</strong>
|
||||
</a>{" "}
|
||||
(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{" "}
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/legalcode" target="_blank" rel="noopener noreferrer">
|
||||
full license
|
||||
</a>{" "}
|
||||
is re-printed below.
|
||||
</p>
|
||||
<hr />
|
||||
<h2>Creative Commons Attribution 4.0 International Public License</h2>
|
||||
<p className="center">
|
||||
<a
|
||||
className="no-underline"
|
||||
href="https://creativecommons.org/licenses/by/4.0/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Creative Commons Attribution 4.0"
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
</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>
|
||||
<ul>
|
||||
<li>
|
||||
<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.{" "}
|
||||
<a
|
||||
href="https://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
More considerations for licensors
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<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.{" "}
|
||||
<a
|
||||
href="https://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
More considerations for the public
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
<ol>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Term.</strong> The term of this Public License is specified in Section 6(a).
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
b.{" "}
|
||||
<em>
|
||||
<strong>Other rights.</strong>
|
||||
</em>
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>Patent and trademark rights are not licensed under this Public License.</p>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
</ol>
|
||||
<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>
|
||||
<ol>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
</ol>
|
||||
<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>
|
||||
<ol>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<p>upon express reinstatement by the Licensor.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<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{" "}
|
||||
<a
|
||||
href="https://creativecommons.org/publicdomain/zero/1.0/legalcode"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<em>CC0 Public Domain Dedication</em>
|
||||
</a>
|
||||
. 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{" "}
|
||||
<a href="https://creativecommons.org/policies" target="_blank" rel="noopener noreferrer">
|
||||
creativecommons.org/policies
|
||||
</a>
|
||||
, 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{" "}
|
||||
<a href="https://creativecommons.org/" target="_blank" rel="noopener noreferrer">
|
||||
creativecommons.org
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</blockquote>
|
||||
</Content>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
}
|
113
pages/notes/[slug].tsx
Normal file
113
pages/notes/[slug].tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import matter from "gray-matter";
|
||||
import { MDXRemote } from "next-mdx-remote";
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
import { NextSeo, ArticleJsonLd } from "next-seo";
|
||||
import Layout from "../../components/Layout";
|
||||
import Container from "../../components/Container";
|
||||
import Content from "../../components/Content";
|
||||
import Meta from "../../components/notes/Meta";
|
||||
import { notePaths, NOTES_PATH } from "../../lib/parseNotes";
|
||||
import mdxComponents from "../../components/mdxComponents";
|
||||
import * as config from "../../lib/config";
|
||||
import type { GetStaticProps, GetStaticPaths } from "next";
|
||||
|
||||
// mdx plugins
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import rehypeExternalLinks from "rehype-external-links";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
|
||||
export default function Page({ source, frontMatter, slug }) {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={frontMatter.title}
|
||||
description={frontMatter.description}
|
||||
openGraph={{
|
||||
title: frontMatter.title,
|
||||
type: "article",
|
||||
article: {
|
||||
publishedTime: frontMatter.date,
|
||||
},
|
||||
images: [
|
||||
{
|
||||
url: `${config.baseURL}${frontMatter.image}`,
|
||||
alt: frontMatter.title,
|
||||
},
|
||||
],
|
||||
}}
|
||||
twitter={{
|
||||
handle: `@${config.twitterHandle}`,
|
||||
site: `@${config.twitterHandle}`,
|
||||
cardType: "summary_large_image",
|
||||
}}
|
||||
/>
|
||||
<ArticleJsonLd
|
||||
url={`${config.baseURL}/notes/${slug}`}
|
||||
title={frontMatter.title}
|
||||
description={frontMatter.description}
|
||||
datePublished={frontMatter.date}
|
||||
dateModified={frontMatter.date}
|
||||
images={[`${config.baseURL}${frontMatter.image}`]}
|
||||
authorName={[config.authorName]}
|
||||
publisherName={config.siteName}
|
||||
publisherLogo={`${config.baseURL}/static/images/me.jpg`}
|
||||
/>
|
||||
|
||||
<Layout>
|
||||
<Container>
|
||||
<Meta {...frontMatter} slug={slug} />
|
||||
<Content>
|
||||
<MDXRemote {...source} components={mdxComponents} />
|
||||
</Content>
|
||||
</Container>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||
const filePath = path.join(NOTES_PATH, `${params.slug}.mdx`);
|
||||
const source = fs.readFileSync(filePath);
|
||||
|
||||
const { content, data } = matter(source);
|
||||
|
||||
const mdxSource = await serialize(content, {
|
||||
scope: data,
|
||||
mdxOptions: {
|
||||
// remarkPlugins: [],
|
||||
rehypePlugins: [
|
||||
[rehypeExternalLinks, { target: "_blank", rel: ["noopener", "noreferrer"] }],
|
||||
[rehypeSlug, {}],
|
||||
[
|
||||
rehypeAutolinkHeadings,
|
||||
{ behavior: "append", properties: { className: "h-anchor" }, content: [], test: ["h2", "h3"] },
|
||||
],
|
||||
[rehypeHighlight, {}],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
frontMatter: data,
|
||||
source: mdxSource,
|
||||
slug: params.slug,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const paths = notePaths
|
||||
// Remove file extensions for page paths
|
||||
.map((path) => path.replace(/\.mdx?$/, ""))
|
||||
// Map the path into the static paths object required by Next.js
|
||||
.map((slug) => ({ params: { slug } }));
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
};
|
||||
};
|
32
pages/notes/index.tsx
Normal file
32
pages/notes/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { format, parseISO } from "date-fns";
|
||||
import groupBy from "lodash.groupby";
|
||||
import Layout from "../../components/Layout";
|
||||
import Container from "../../components/Container";
|
||||
import List from "../../components/notes/List";
|
||||
import { getAllNotes } from "../../lib/parseNotes";
|
||||
import type { GetStaticProps } from "next";
|
||||
|
||||
export default function Notes({ allNotes }) {
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Container title="Notes" description="Recent posts by Jake Jarvis.">
|
||||
<List allNotes={allNotes} />
|
||||
</Container>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
const allNotes = getAllNotes(["date", "slug", "title"]);
|
||||
|
||||
// parse year of each note
|
||||
allNotes.map((note: any) => (note.year = parseInt(format(parseISO(note.date), "yyyy"))));
|
||||
|
||||
return {
|
||||
props: {
|
||||
allNotes: groupBy(allNotes, "year"),
|
||||
},
|
||||
};
|
||||
};
|
248
pages/previously.tsx
Normal file
248
pages/previously.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import Image from "next/image";
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import Content from "../components/Content";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import { FloppyIcon, SirenIcon } from "../components/icons";
|
||||
|
||||
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";
|
||||
|
||||
export default function Previously() {
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Container
|
||||
title="Previously on..."
|
||||
description="An incredibly embarrassing and somewhat painful down of this site's memory lane..."
|
||||
>
|
||||
<PageTitle
|
||||
title={
|
||||
<>
|
||||
<FloppyIcon /> Previously on...
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Content>
|
||||
<figure>
|
||||
<a
|
||||
className="no-underline"
|
||||
href="https://web.archive.org/web/20010501000000*/jakejarvis.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image src={img_wayback} placeholder="blur" alt="Timeline of this website's past." />
|
||||
</a>
|
||||
<figcaption>
|
||||
...the{" "}
|
||||
<a
|
||||
href="https://web.archive.org/web/20010501000000*/jakejarvis.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Cringey Chronicles™
|
||||
</a>{" "}
|
||||
of this website's past.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
<SirenIcon /> <strong>Trigger warning:</strong> marquees, Comic Sans MS, popups,{" "}
|
||||
<code>
|
||||
color: <span className="limegreen">limegreen</span>
|
||||
</code>
|
||||
...{" "}
|
||||
<a href="https://y2k.app/" target="_blank" rel="noopener noreferrer">
|
||||
Click for the{" "}
|
||||
<strong>
|
||||
<em>FULL</em>
|
||||
</strong>{" "}
|
||||
experience anyway.
|
||||
</a>
|
||||
</p>
|
||||
<figure>
|
||||
<iframe
|
||||
className="y2k_frame"
|
||||
src="https://jakejarvis.github.io/my-first-website/"
|
||||
title="My Terrible, Horrible, No Good, Very Bad First Website"
|
||||
></iframe>
|
||||
<figcaption>
|
||||
November 2001 (
|
||||
<a href="https://github.com/jakejarvis/my-first-website" target="_blank" rel="noopener noreferrer">
|
||||
archived source
|
||||
</a>
|
||||
)
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2002_02} placeholder="blur" alt="February 2002" />
|
||||
<figcaption>February 2002</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2002_10} placeholder="blur" alt="October 2002" />
|
||||
<figcaption>October 2002</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2003_08} placeholder="blur" alt="August 2003" />
|
||||
<figcaption>August 2003</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2004_11} placeholder="blur" alt="November 2004" />
|
||||
<figcaption>November 2004</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2006_04} placeholder="blur" alt="April 2006" />
|
||||
<figcaption>April 2006</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2006_05} placeholder="blur" alt="May 2006" />
|
||||
<figcaption>May 2006</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2007_01} placeholder="blur" alt="January 2007" />
|
||||
<figcaption>January 2007</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2007_04} placeholder="blur" alt="April 2007" />
|
||||
<figcaption>April 2007</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2007_05} placeholder="blur" alt="May 2007" />
|
||||
<figcaption>May 2007</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<Image src={img_2009_07} placeholder="blur" alt="July 2009" />
|
||||
<figcaption>July 2009</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<a
|
||||
className="no-underline"
|
||||
href="https://github.com/jakejarvis/jarv.is/tree/v1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image src={img_2012_09} placeholder="blur" alt="September 2012" />
|
||||
</a>
|
||||
<figcaption>
|
||||
September 2012 (
|
||||
<a href="https://github.com/jakejarvis/jarv.is/tree/v1" target="_blank" rel="noopener noreferrer">
|
||||
archived source
|
||||
</a>
|
||||
)
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr />
|
||||
|
||||
<figure>
|
||||
<a
|
||||
className="no-underline"
|
||||
href="https://github.com/jakejarvis/jarv.is/tree/v2"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image src={img_2018_04} placeholder="blur" alt="April 2018" />
|
||||
</a>
|
||||
<figcaption>
|
||||
April 2018 (
|
||||
<a href="https://github.com/jakejarvis/jarv.is/tree/v2" target="_blank" rel="noopener noreferrer">
|
||||
archived source
|
||||
</a>
|
||||
)
|
||||
</figcaption>
|
||||
</figure>
|
||||
</Content>
|
||||
</Container>
|
||||
</Layout>
|
||||
<style jsx global>{`
|
||||
body {
|
||||
font-family: "Comic Neue", "Comic Sans MS", "Comic Sans", "Inter", sans-serif;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
header nav a span:nth-of-type(2) {
|
||||
font-size: 1.1em;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
header nav > a span:nth-of-type(2) {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
main > div > div {
|
||||
font-size: 1.1em !important;
|
||||
text-align: center;
|
||||
}
|
||||
main > div > div p {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
main > div > div strong {
|
||||
font-weight: 900;
|
||||
}
|
||||
main > div > div code {
|
||||
font-size: 0.85em;
|
||||
font-weight: 400;
|
||||
}
|
||||
main > div > div figure:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
footer > div {
|
||||
font-size: 0.95em !important;
|
||||
}
|
||||
.y2k_frame {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border: 2px solid #e3d18c;
|
||||
}
|
||||
.limegreen {
|
||||
color: #32cd32;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
178
pages/privacy.tsx
Normal file
178
pages/privacy.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import Content from "../components/Content";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import { PrivacyIcon } from "../components/icons";
|
||||
|
||||
import faunaImg from "../public/static/images/privacy/fauna_hits.png";
|
||||
|
||||
export default function Privacy() {
|
||||
return (
|
||||
<Layout>
|
||||
<Container title="Privacy">
|
||||
<PageTitle
|
||||
title={
|
||||
<>
|
||||
<PrivacyIcon /> Privacy
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Content>
|
||||
<p>Okay, this is an easy one. 😉</p>
|
||||
<h2 id="analytics">Analytics</h2>
|
||||
<p>
|
||||
A simple hit counter on each page tallies an aggregate number of pageviews (i.e.{" "}
|
||||
<code>hits = hits + 1</code>). Individual views and identifying (or non-identifying) details are{" "}
|
||||
<strong>never stored or logged</strong>.
|
||||
</p>
|
||||
<p>
|
||||
The{" "}
|
||||
<a
|
||||
href="https://github.com/jakejarvis/jarv.is/blob/main/api/hits.js"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
serverless function
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="https://github.com/jakejarvis/jarv.is/blob/main/assets/js/src/components/Counter.js"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
client script
|
||||
</a>{" "}
|
||||
are open source, and{" "}
|
||||
<a href="https://github.com/jakejarvis/website-stats" target="_blank" rel="noopener noreferrer">
|
||||
snapshots of the database
|
||||
</a>{" "}
|
||||
are public.
|
||||
</p>
|
||||
<Image src={faunaImg} placeholder="blur" alt="The entire database schema." />
|
||||
<h2 id="hosting">Hosting</h2>
|
||||
<p>
|
||||
Pages and first-party assets on this website are served by{" "}
|
||||
<a href="https://vercel.com/" target="_blank" rel="noopener noreferrer">
|
||||
<strong>▲ Vercel</strong>
|
||||
</a>
|
||||
. Refer to their{" "}
|
||||
<a href="https://vercel.com/legal/privacy-policy" target="_blank" rel="noopener noreferrer">
|
||||
privacy policy
|
||||
</a>{" "}
|
||||
for more information.
|
||||
</p>
|
||||
<p>
|
||||
For a likely excessive level of privacy and security, this website is also mirrored on the{" "}
|
||||
<a href="https://www.torproject.org/" target="_blank" rel="noopener noreferrer">
|
||||
🧅 Tor network
|
||||
</a>{" "}
|
||||
at:
|
||||
</p>
|
||||
<blockquote>
|
||||
<p>
|
||||
<a
|
||||
href="http://jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<strong>jarvis2i2vp4j4tbxjogsnqdemnte5xhzyi7hziiyzxwge3hzmh57zad.onion</strong>
|
||||
</a>
|
||||
</p>
|
||||
</blockquote>
|
||||
<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>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://blog.codepen.io/documentation/privacy/" target="_blank" rel="noopener noreferrer">
|
||||
CodePen
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.facebook.com/policy.php" target="_blank" rel="noopener noreferrer">
|
||||
Facebook
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://docs.github.com/en/github/site-policy/github-privacy-statement"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://soundcloud.com/pages/privacy" target="_blank" rel="noopener noreferrer">
|
||||
SoundCloud
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/en/privacy" target="_blank" rel="noopener noreferrer">
|
||||
Twitter
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://vimeo.com/privacy" target="_blank" rel="noopener noreferrer">
|
||||
Vimeo
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">
|
||||
YouTube
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="hcaptcha">Fighting Spam</h2>
|
||||
<p>
|
||||
Using{" "}
|
||||
<a href="https://www.hcaptcha.com/" target="_blank" rel="noopener noreferrer">
|
||||
<strong>hCaptcha</strong>
|
||||
</a>{" "}
|
||||
to fight bot spam on the <Link href="/contact/">contact form</Link> was an easy choice over seemingly
|
||||
unavoidable alternatives like{" "}
|
||||
<a href="https://developers.google.com/recaptcha/" target="_blank" rel="noopener noreferrer">
|
||||
reCAPTCHA
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
You can refer to hCaptcha's{" "}
|
||||
<a href="https://www.hcaptcha.com/privacy" target="_blank" rel="noopener noreferrer">
|
||||
privacy policy
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="https://www.hcaptcha.com/terms" target="_blank" rel="noopener noreferrer">
|
||||
terms of service
|
||||
</a>{" "}
|
||||
for more details. While some information is sent to the hCaptcha API about your behavior{" "}
|
||||
<strong>(on the contact page only)</strong>, at least you won't be helping a certain internet conglomerate{" "}
|
||||
<a
|
||||
href="https://blog.cloudflare.com/moving-from-recaptcha-to-hcaptcha/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
train their self-driving cars
|
||||
</a>
|
||||
. 🚗
|
||||
</p>
|
||||
<p>
|
||||
I also enabled the setting to donate 100% of my{" "}
|
||||
<a href="https://humanprotocol.org/?lng=en-US" target="_blank" rel="noopener noreferrer">
|
||||
HMT token
|
||||
</a>{" "}
|
||||
earnings to the{" "}
|
||||
<a href="https://wikimediafoundation.org/" target="_blank" rel="noopener noreferrer">
|
||||
Wikimedia Foundation
|
||||
</a>
|
||||
, for what it's worth. (A few cents, probably... 💰)
|
||||
</p>
|
||||
</Content>
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
}
|
52
pages/projects.tsx
Normal file
52
pages/projects.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import useSWR from "swr";
|
||||
import { fetcher } from "../lib/fetcher";
|
||||
import Layout from "../components/Layout";
|
||||
import Container from "../components/Container";
|
||||
import PageTitle from "../components/page/PageTitle";
|
||||
import RepositoryGrid from "../components/projects/RepositoryGrid";
|
||||
import Loading from "../components/loading/Loading";
|
||||
import { ProjectsIcon } from "../components/icons";
|
||||
|
||||
function Grid() {
|
||||
// start fetching repos from API immediately
|
||||
const { data, error } = useSWR("/api/projects/?sort=top&limit=12", fetcher);
|
||||
|
||||
if (error) {
|
||||
return <div>error: {error.message}</div>;
|
||||
}
|
||||
|
||||
// show spinning loading indicator if data isn't fetched yet
|
||||
if (!data) {
|
||||
return (
|
||||
<div>
|
||||
<Loading boxes={3} width={50} />
|
||||
<style jsx>{`
|
||||
div {
|
||||
text-align: center;
|
||||
margin: 2.5em auto;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// we have data!
|
||||
return <RepositoryGrid repos={data} />;
|
||||
}
|
||||
|
||||
export default function Projects() {
|
||||
return (
|
||||
<Layout>
|
||||
<Container title="👨💻 Projects">
|
||||
<PageTitle
|
||||
title={
|
||||
<>
|
||||
<ProjectsIcon /> Projects
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Grid />
|
||||
</Container>
|
||||
</Layout>
|
||||
);
|
||||
}
|
1099
pages/uses.tsx
Normal file
1099
pages/uses.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user