mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-04-17 09:28:43 -04:00
next-mdx-remote v4 (#737)
This commit is contained in:
97
components/CodeBlock/CodeBlock.module.css
Normal file
97
components/CodeBlock/CodeBlock.module.css
Normal file
@@ -0,0 +1,97 @@
|
||||
.code {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
/* the following sub-classes MUST be global -- the highlight rehype plugin isn't aware of this file */
|
||||
|
||||
.code :global(.code-highlight) {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em;
|
||||
tab-size: 2;
|
||||
color: var(--code-text);
|
||||
background-color: var(--code-background);
|
||||
}
|
||||
|
||||
/* leave room for clipboard button to the right of the first line */
|
||||
.code :global(.code-highlight) > :global(.code-line:first-of-type) {
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) > :global(.code-line.line-number::before) {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
text-align: right;
|
||||
color: var(--code-comment);
|
||||
content: attr(line); /* added to spans by prism */
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.comment),
|
||||
.code :global(.code-highlight) :global(.token.prolog),
|
||||
.code :global(.code-highlight) :global(.token.cdata) {
|
||||
color: var(--code-comment);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.delimiter),
|
||||
.code :global(.code-highlight) :global(.token.boolean),
|
||||
.code :global(.code-highlight) :global(.token.keyword),
|
||||
.code :global(.code-highlight) :global(.token.selector),
|
||||
.code :global(.code-highlight) :global(.token.important),
|
||||
.code :global(.code-highlight) :global(.token.doctype),
|
||||
.code :global(.code-highlight) :global(.token.atrule),
|
||||
.code :global(.code-highlight) :global(.token.url) {
|
||||
color: var(--code-keyword);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.tag),
|
||||
.code :global(.code-highlight) :global(.token.builtin),
|
||||
.code :global(.code-highlight) :global(.token.regex) {
|
||||
color: var(--code-namespace);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.property),
|
||||
.code :global(.code-highlight) :global(.token.constant),
|
||||
.code :global(.code-highlight) :global(.token.variable),
|
||||
.code :global(.code-highlight) :global(.token.attr-value),
|
||||
.code :global(.code-highlight) :global(.token.class-name),
|
||||
.code :global(.code-highlight) :global(.token.string),
|
||||
.code :global(.code-highlight) :global(.token.char) {
|
||||
color: var(--code-variable);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.literal-property),
|
||||
.code :global(.code-highlight) :global(.token.attr-name) {
|
||||
color: var(--code-attribute);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.function) {
|
||||
color: var(--code-literal);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.tag .punctuation),
|
||||
.code :global(.code-highlight) :global(.token.attr-value .punctuation) {
|
||||
color: var(--code-punctuation);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.inserted) {
|
||||
background-color: var(--code-addition);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.deleted) {
|
||||
background-color: var(--code-deletion);
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.url) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.bold) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.code :global(.code-highlight) :global(.token.italic) {
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import CopyButton from "./CopyButton";
|
||||
import CopyButton from "../CopyButton/CopyButton";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import styles from "./Code.module.css";
|
||||
import styles from "./CodeBlock.module.css";
|
||||
|
||||
export type CustomCodeProps = {
|
||||
export type Props = {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const CustomCode = (props: CustomCodeProps) => {
|
||||
const CodeBlock = (props: Props) => {
|
||||
if (props.className?.split(" ").includes("code-highlight")) {
|
||||
// full multi-line code blocks with prism highlighting and copy-to-clipboard button
|
||||
return (
|
||||
<>
|
||||
<div className={styles.code_block}>
|
||||
<div className={styles.code}>
|
||||
<CopyButton source={props.children} />
|
||||
<code {...props}>{props.children}</code>
|
||||
</div>
|
||||
@@ -25,4 +25,4 @@ const CustomCode = (props: CustomCodeProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
export default CustomCode;
|
||||
export default CodeBlock;
|
||||
@@ -3,7 +3,7 @@ import Link from "next/link";
|
||||
import css from "styled-jsx/css";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type ColorLinkProps = {
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
href: string;
|
||||
lightColor: string;
|
||||
@@ -12,7 +12,7 @@ type ColorLinkProps = {
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
const getFancyLinkStyles = ({ lightColor, darkColor }: Partial<ColorLinkProps>) => {
|
||||
const getFancyLinkStyles = ({ lightColor, darkColor }: Partial<Props>) => {
|
||||
// spits out a linear-gradient (that's not realy a gradient) with translucent color in rgba() format
|
||||
const linearGradient = (hex: string, alpha = 0.4) => {
|
||||
// hex -> rgb, adapted from https://github.com/sindresorhus/hex-rgb/blob/main/index.js
|
||||
@@ -39,7 +39,7 @@ const getFancyLinkStyles = ({ lightColor, darkColor }: Partial<ColorLinkProps>)
|
||||
`;
|
||||
};
|
||||
|
||||
const ColorLink = ({ href, title, lightColor, darkColor, external = false, children }: ColorLinkProps) => {
|
||||
const ColorfulLink = ({ href, title, lightColor, darkColor, external = false, children }: Props) => {
|
||||
const { className, styles } = getFancyLinkStyles({ lightColor, darkColor });
|
||||
|
||||
return (
|
||||
@@ -60,4 +60,4 @@ const ColorLink = ({ href, title, lightColor, darkColor, external = false, child
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ColorLink);
|
||||
export default memo(ColorfulLink);
|
||||
@@ -4,7 +4,7 @@ import classNames from "classnames/bind";
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import isEmailLike from "is-email-like";
|
||||
import { SendIcon, CheckOcticon, XOcticon } from "../icons";
|
||||
import { SendIcon, CheckOcticon, XOcticon } from "../Icons";
|
||||
|
||||
import type { FormikHelpers } from "formik";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styles from "./Content.module.css";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import styles from "./Content.module.css";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import classNames from "classnames/bind";
|
||||
import copy from "copy-to-clipboard";
|
||||
import innerText from "react-innertext";
|
||||
import { ClipboardOcticon, CheckOcticon } from "../icons";
|
||||
import { ClipboardOcticon, CheckOcticon } from "../Icons";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import styles from "./CopyButton.module.css";
|
||||
@@ -1,14 +1,14 @@
|
||||
import Image from "./Image";
|
||||
import Image from "../Image/Image";
|
||||
import innerText from "react-innertext";
|
||||
import type { ReactNode } from "react";
|
||||
import type { ImageProps } from "next/image";
|
||||
import type { ImageProps as NextImageProps } from "next/image";
|
||||
|
||||
type CustomFigureProps = Omit<ImageProps, "alt"> & {
|
||||
type Props = Omit<NextImageProps, "alt"> & {
|
||||
children: ReactNode; // caption (can be in markdown, yay!!!)
|
||||
alt?: string; // becomes optional -- pulled from plaintext-ified caption if missing
|
||||
};
|
||||
|
||||
const CustomFigure = ({ children, alt, ...imageProps }: CustomFigureProps) => {
|
||||
const Figure = ({ children, alt, ...imageProps }: Props) => {
|
||||
return (
|
||||
<figure>
|
||||
<Image alt={alt || innerText(children)} {...imageProps} />
|
||||
@@ -17,4 +17,4 @@ const CustomFigure = ({ children, alt, ...imageProps }: CustomFigureProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomFigure;
|
||||
export default Figure;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import { HeartIcon, NextjsLogo } from "../icons";
|
||||
import { HeartIcon, NextjsLogo } from "../Icons";
|
||||
import * as config from "../../lib/config";
|
||||
|
||||
import styles from "./Footer.module.css";
|
||||
3
components/GistEmbed/GistEmbed.tsx
Normal file
3
components/GistEmbed/GistEmbed.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import GistEmbed from "react-gist";
|
||||
|
||||
export default GistEmbed;
|
||||
52
components/Header/Header.module.css
Normal file
52
components/Header/Header.module.css
Normal file
@@ -0,0 +1,52 @@
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4.5em;
|
||||
padding: 0.7em 1.5em;
|
||||
border-bottom: 1px solid var(--kinda-light);
|
||||
background-color: var(--background-header);
|
||||
backdrop-filter: saturate(180%) blur(5px);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 865px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav > div {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0.75em 1.25em;
|
||||
height: 5.9em;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
max-width: 325px;
|
||||
margin-left: 2.5em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
.right {
|
||||
max-width: 225px;
|
||||
margin-left: 1.6em;
|
||||
}
|
||||
}
|
||||
21
components/Header/Header.tsx
Normal file
21
components/Header/Header.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { memo } from "react";
|
||||
import Name from "../Name/Name";
|
||||
import Menu from "../Menu/Menu";
|
||||
|
||||
import styles from "./Header.module.css";
|
||||
|
||||
const Header = () => (
|
||||
<header className={styles.header}>
|
||||
<nav className={styles.nav}>
|
||||
<div className={styles.left}>
|
||||
<Name />
|
||||
</div>
|
||||
|
||||
<div className={styles.right}>
|
||||
<Menu />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
|
||||
export default memo(Header);
|
||||
@@ -1,5 +1,5 @@
|
||||
import useSWR from "swr";
|
||||
import Loading from "../loading/Loading";
|
||||
import Loading from "../Loading/Loading";
|
||||
import { fetcher } from "../../lib/fetcher";
|
||||
|
||||
const HitCounter = ({ slug }) => {
|
||||
21
components/Image/Image.tsx
Normal file
21
components/Image/Image.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import NextImage from "next/image";
|
||||
import type { ImageProps as NextImageProps } from "next/image";
|
||||
|
||||
const Image = ({ src, width, height, alt, quality, priority }: NextImageProps) => {
|
||||
return (
|
||||
<div className="image_wrapper">
|
||||
<NextImage
|
||||
src={(src as string).replace(/^\/public/g, "")}
|
||||
layout="intrinsic"
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt || ""}
|
||||
quality={quality || 65}
|
||||
loading={priority ? "eager" : "lazy"}
|
||||
priority={!!priority}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Image;
|
||||
@@ -1,8 +1,8 @@
|
||||
import Head from "next/head";
|
||||
import { useTheme } from "next-themes";
|
||||
import Header from "./header/Header";
|
||||
import Footer from "./footer/Footer";
|
||||
import { themeColors } from "../lib/config";
|
||||
import Header from "../Header/Header";
|
||||
import Footer from "../Footer/Footer";
|
||||
import { themeColors } from "../../lib/config";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import styles from "./Layout.module.css";
|
||||
71
components/Menu/Menu.module.css
Normal file
71
components/Menu/Menu.module.css
Normal file
@@ -0,0 +1,71 @@
|
||||
.menu {
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu li {
|
||||
list-style: none;
|
||||
display: inline-flex;
|
||||
margin-left: 1.8em;
|
||||
}
|
||||
|
||||
.menu li .link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--medium-dark);
|
||||
background: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.menu li .link:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.menu li .icon {
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
}
|
||||
|
||||
.menu li span {
|
||||
font-size: 0.95em;
|
||||
font-weight: 500;
|
||||
margin-left: 0.8em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.menu li.theme_toggle {
|
||||
margin-left: 1.25em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.menu {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu li {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.menu li .icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
}
|
||||
|
||||
/* hide text next to emojis on mobile */
|
||||
.menu li span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu li.theme_toggle {
|
||||
margin-left: -0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
/* the home icon is redundant when space is SUPER tight */
|
||||
@media screen and (max-width: 380px) {
|
||||
.menu li:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
49
components/Menu/Menu.tsx
Normal file
49
components/Menu/Menu.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import ThemeToggle from "../ThemeToggle/ThemeToggle";
|
||||
import { HomeIcon, NotesIcon, ProjectsIcon, ContactIcon } from "../Icons";
|
||||
|
||||
import styles from "./Menu.module.css";
|
||||
|
||||
const links = [
|
||||
{
|
||||
icon: <HomeIcon className={`icon ${styles.icon}`} />,
|
||||
text: "Home",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
icon: <NotesIcon className={`icon ${styles.icon}`} />,
|
||||
text: "Notes",
|
||||
href: "/notes/",
|
||||
},
|
||||
{
|
||||
icon: <ProjectsIcon className={`icon ${styles.icon}`} />,
|
||||
text: "Projects",
|
||||
href: "/projects/",
|
||||
},
|
||||
{
|
||||
icon: <ContactIcon className={`icon ${styles.icon}`} />,
|
||||
text: "Contact",
|
||||
href: "/contact/",
|
||||
},
|
||||
];
|
||||
|
||||
const Menu = () => (
|
||||
<ul className={styles.menu}>
|
||||
{links.map((link, index) => (
|
||||
<li key={index}>
|
||||
<Link href={link.href} prefetch={false}>
|
||||
<a className={styles.link}>
|
||||
{link.icon} <span>{link.text}</span>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className={styles.theme_toggle}>
|
||||
<ThemeToggle className={styles.icon} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
export default memo(Menu);
|
||||
45
components/Name/Name.module.css
Normal file
45
components/Name/Name.module.css
Normal file
@@ -0,0 +1,45 @@
|
||||
.name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--medium-dark);
|
||||
background: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.name .selfie {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.name .selfie img {
|
||||
border: 1px solid var(--light) !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.name:hover .selfie {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.name span:last-of-type {
|
||||
margin: 0 0.6em;
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.name .selfie {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.name span:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
20
components/Name/Name.tsx
Normal file
20
components/Name/Name.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import styles from "./Name.module.css";
|
||||
|
||||
import meJpg from "../../public/static/images/me.jpg";
|
||||
|
||||
const Name = () => (
|
||||
<Link href="/">
|
||||
<a className={styles.name}>
|
||||
<div className={styles.selfie}>
|
||||
<Image src={meJpg} alt="Photo of Jake Jarvis" width={70} height={70} quality={60} layout="intrinsic" priority />
|
||||
</div>
|
||||
<span>Jake Jarvis</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
||||
export default memo(Name);
|
||||
@@ -1,13 +1,13 @@
|
||||
import Link from "next/link";
|
||||
import { format } from "date-fns";
|
||||
import HitCounter from "./HitCounter";
|
||||
import { DateIcon, TagIcon, EditIcon, ViewsIcon } from "../icons";
|
||||
import HitCounter from "../HitCounter/HitCounter";
|
||||
import { DateIcon, TagIcon, EditIcon, ViewsIcon } from "../Icons";
|
||||
import * as config from "../../lib/config";
|
||||
import type { NoteMetaType } from "../../types";
|
||||
|
||||
import styles from "./Meta.module.css";
|
||||
import styles from "./NoteMeta.module.css";
|
||||
|
||||
const Meta = ({ slug, date, title, htmlTitle, tags = [] }: NoteMetaType) => (
|
||||
const NoteMeta = ({ slug, date, title, htmlTitle, tags = [] }: NoteMetaType) => (
|
||||
<>
|
||||
<div className={styles.meta}>
|
||||
<div className={styles.date}>
|
||||
@@ -66,4 +66,4 @@ const Meta = ({ slug, date, title, htmlTitle, tags = [] }: NoteMetaType) => (
|
||||
</>
|
||||
);
|
||||
|
||||
export default Meta;
|
||||
export default NoteMeta;
|
||||
@@ -2,9 +2,9 @@ import Link from "next/link";
|
||||
import { format } from "date-fns";
|
||||
import type { NoteMetaType } from "../../types";
|
||||
|
||||
import styles from "./List.module.css";
|
||||
import styles from "./NotesList.module.css";
|
||||
|
||||
const List = ({ notesByYear }) => {
|
||||
const NotesList = ({ notesByYear }) => {
|
||||
const sections = [];
|
||||
|
||||
Object.entries(notesByYear).forEach(([year, notes]: [string, NoteMetaType[]]) => {
|
||||
@@ -39,4 +39,4 @@ const List = ({ notesByYear }) => {
|
||||
return <>{reversed}</>;
|
||||
};
|
||||
|
||||
export default List;
|
||||
export default NotesList;
|
||||
19
components/OctocatLink/OctocatLink.tsx
Normal file
19
components/OctocatLink/OctocatLink.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { OctocatOcticon } from "../Icons";
|
||||
|
||||
type Props = {
|
||||
repo: string;
|
||||
};
|
||||
|
||||
const OctocatLink = (props: Props) => (
|
||||
<a
|
||||
className="no-underline"
|
||||
href={`https://github.com/${props.repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ margin: "0 0.4em", color: "var(--text)" }}
|
||||
>
|
||||
<OctocatOcticon fill="currentColor" />
|
||||
</a>
|
||||
);
|
||||
|
||||
export default OctocatLink;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { intlFormat, formatDistanceToNowStrict } from "date-fns";
|
||||
import { StarOcticon, ForkOcticon } from "../icons";
|
||||
import { StarOcticon, ForkOcticon } from "../Icons";
|
||||
import { RepoType } from "../../types";
|
||||
|
||||
import styles from "./RepoCard.module.css";
|
||||
import styles from "./RepositoryCard.module.css";
|
||||
|
||||
const RepoCard = (props: RepoType) => (
|
||||
const RepositoryCard = (props: RepoType) => (
|
||||
<div className={styles.card}>
|
||||
<a className={styles.name} href={props.url} target="_blank" rel="noopener noreferrer">
|
||||
{props.name}
|
||||
@@ -71,4 +71,4 @@ const RepoCard = (props: RepoType) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RepoCard;
|
||||
export default RepositoryCard;
|
||||
17
components/TweetEmbed/TweetEmbed.tsx
Normal file
17
components/TweetEmbed/TweetEmbed.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import Tweet from "react-tweet-embed";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const TweetEmbed = (props: Props) => (
|
||||
<Tweet
|
||||
id={props.id}
|
||||
options={{
|
||||
dnt: true,
|
||||
align: "center",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default TweetEmbed;
|
||||
10
components/Video/Video.module.css
Normal file
10
components/Video/Video.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
padding-top: 56.25%;
|
||||
}
|
||||
|
||||
.wrapper > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
63
components/Video/Video.tsx
Normal file
63
components/Video/Video.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import ReactPlayer from "react-player/file";
|
||||
|
||||
import styles from "./Video.module.css";
|
||||
|
||||
type Props = {
|
||||
webm?: string;
|
||||
mp4?: string;
|
||||
thumbnail?: string;
|
||||
subs?: string;
|
||||
autoplay?: boolean;
|
||||
};
|
||||
|
||||
const Video = ({ webm, mp4, thumbnail, subs, autoplay }: Props) => {
|
||||
const url = [
|
||||
webm && {
|
||||
src: webm,
|
||||
type: "video/webm",
|
||||
},
|
||||
mp4 && {
|
||||
src: mp4,
|
||||
type: "video/mp4",
|
||||
},
|
||||
];
|
||||
|
||||
const config = {
|
||||
file: {
|
||||
attributes: {
|
||||
controlsList: "nodownload",
|
||||
preload: "metadata",
|
||||
autoPlay: autoplay,
|
||||
muted: autoplay,
|
||||
loop: autoplay,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (thumbnail) {
|
||||
// @ts-ignore
|
||||
config.file.attributes.poster = thumbnail;
|
||||
}
|
||||
|
||||
if (subs) {
|
||||
// @ts-ignore
|
||||
config.file.tracks = [
|
||||
{
|
||||
kind: "subtitles",
|
||||
src: subs,
|
||||
srcLang: "en",
|
||||
label: "English",
|
||||
default: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{/* @ts-ignore */}
|
||||
<ReactPlayer width="100%" height="100%" url={url} config={config} controls={!autoplay} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Video;
|
||||
10
components/YouTubeEmbed/YouTubeEmbed.module.css
Normal file
10
components/YouTubeEmbed/YouTubeEmbed.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
padding-top: 56.25%;
|
||||
}
|
||||
|
||||
.wrapper > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
21
components/YouTubeEmbed/YouTubeEmbed.tsx
Normal file
21
components/YouTubeEmbed/YouTubeEmbed.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import ReactPlayer from "react-player/youtube";
|
||||
|
||||
import styles from "./YouTubeEmbed.module.css";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const YouTubeEmbed = ({ id }: Props) => (
|
||||
<div className={styles.wrapper}>
|
||||
<ReactPlayer
|
||||
width="100%"
|
||||
height="100%"
|
||||
url={`https://www.youtube-nocookie.com/watch?v=${id}`}
|
||||
light={`https://i.ytimg.com/vi/${id}/hqdefault.jpg`}
|
||||
controls
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default YouTubeEmbed;
|
||||
@@ -1,97 +0,0 @@
|
||||
.code_block {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
/* the following sub-classes MUST be global -- the highlight rehype plugin isn't aware of this file */
|
||||
|
||||
.code_block :global(.code-highlight) {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em;
|
||||
tab-size: 2;
|
||||
color: var(--code-text);
|
||||
background-color: var(--code-background);
|
||||
}
|
||||
|
||||
/* leave room for clipboard button to the right of the first line */
|
||||
.code_block :global(.code-highlight) > :global(.code-line:first-of-type) {
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) > :global(.code-line.line-number::before) {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
margin-right: 1.5em;
|
||||
text-align: right;
|
||||
color: var(--code-comment);
|
||||
content: attr(line); /* added to spans by prism */
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.comment),
|
||||
.code_block :global(.code-highlight) :global(.token.prolog),
|
||||
.code_block :global(.code-highlight) :global(.token.cdata) {
|
||||
color: var(--code-comment);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.delimiter),
|
||||
.code_block :global(.code-highlight) :global(.token.boolean),
|
||||
.code_block :global(.code-highlight) :global(.token.keyword),
|
||||
.code_block :global(.code-highlight) :global(.token.selector),
|
||||
.code_block :global(.code-highlight) :global(.token.important),
|
||||
.code_block :global(.code-highlight) :global(.token.doctype),
|
||||
.code_block :global(.code-highlight) :global(.token.atrule),
|
||||
.code_block :global(.code-highlight) :global(.token.url) {
|
||||
color: var(--code-keyword);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.tag),
|
||||
.code_block :global(.code-highlight) :global(.token.builtin),
|
||||
.code_block :global(.code-highlight) :global(.token.regex) {
|
||||
color: var(--code-namespace);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.property),
|
||||
.code_block :global(.code-highlight) :global(.token.constant),
|
||||
.code_block :global(.code-highlight) :global(.token.variable),
|
||||
.code_block :global(.code-highlight) :global(.token.attr-value),
|
||||
.code_block :global(.code-highlight) :global(.token.class-name),
|
||||
.code_block :global(.code-highlight) :global(.token.string),
|
||||
.code_block :global(.code-highlight) :global(.token.char) {
|
||||
color: var(--code-variable);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.literal-property),
|
||||
.code_block :global(.code-highlight) :global(.token.attr-name) {
|
||||
color: var(--code-attribute);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.function) {
|
||||
color: var(--code-literal);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.tag .punctuation),
|
||||
.code_block :global(.code-highlight) :global(.token.attr-value .punctuation) {
|
||||
color: var(--code-punctuation);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.inserted) {
|
||||
background-color: var(--code-addition);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.deleted) {
|
||||
background-color: var(--code-deletion);
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.url) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.bold) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.code_block :global(.code-highlight) :global(.token.italic) {
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4.5em;
|
||||
padding: 0.7em 1.5em;
|
||||
border-bottom: 1px solid var(--kinda-light);
|
||||
background-color: var(--background-header);
|
||||
backdrop-filter: saturate(180%) blur(5px);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 865px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav > div {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
/*** left side: photo/name ***/
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--medium-dark);
|
||||
background: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.name .selfie {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.name .selfie img {
|
||||
border: 1px solid var(--light) !important;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.name:hover .selfie {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.name span:last-of-type {
|
||||
margin: 0 0.6em;
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/*** right side: menu ***/
|
||||
|
||||
.menu {
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu li {
|
||||
list-style: none;
|
||||
display: inline-flex;
|
||||
margin-left: 1.8em;
|
||||
}
|
||||
|
||||
.menu li .link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--medium-dark);
|
||||
background: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.menu li .link:hover {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.menu li .icon {
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
}
|
||||
|
||||
.menu li span {
|
||||
font-size: 0.95em;
|
||||
font-weight: 500;
|
||||
margin-left: 0.8em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.menu li.theme_toggle {
|
||||
margin-left: 1.25em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0.75em 1.25em;
|
||||
height: 5.9em;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.name .selfie {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.name span:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
max-width: 325px;
|
||||
margin-left: 2.5em;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu li {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.menu li .icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
}
|
||||
|
||||
/* hide text next to emojis on mobile */
|
||||
.menu li span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu li.theme_toggle {
|
||||
margin-left: -0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
/* the home icon is redundant when space is SUPER tight */
|
||||
@media screen and (max-width: 380px) {
|
||||
.right {
|
||||
max-width: 225px;
|
||||
margin-left: 1.6em;
|
||||
}
|
||||
|
||||
.menu li:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { memo } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
import { HomeIcon, NotesIcon, ProjectsIcon, ContactIcon } from "../icons";
|
||||
|
||||
import meJpg from "../../public/static/images/me.jpg";
|
||||
|
||||
import styles from "./Header.module.css";
|
||||
|
||||
const links = [
|
||||
{
|
||||
icon: <HomeIcon className={`icon ${styles.icon}`} />,
|
||||
text: "Home",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
icon: <NotesIcon className={`icon ${styles.icon}`} />,
|
||||
text: "Notes",
|
||||
href: "/notes/",
|
||||
},
|
||||
{
|
||||
icon: <ProjectsIcon className={`icon ${styles.icon}`} />,
|
||||
text: "Projects",
|
||||
href: "/projects/",
|
||||
},
|
||||
{
|
||||
icon: <ContactIcon className={`icon ${styles.icon}`} />,
|
||||
text: "Contact",
|
||||
href: "/contact/",
|
||||
},
|
||||
];
|
||||
|
||||
const Header = () => (
|
||||
<header className={styles.header}>
|
||||
<nav className={styles.nav}>
|
||||
<div className={styles.left}>
|
||||
<Link href="/">
|
||||
<a className={styles.name}>
|
||||
<div className={styles.selfie}>
|
||||
<Image
|
||||
src={meJpg}
|
||||
alt="Photo of Jake Jarvis"
|
||||
width={70}
|
||||
height={70}
|
||||
quality={60}
|
||||
layout="intrinsic"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<span>Jake Jarvis</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.right}>
|
||||
<ul className={styles.menu}>
|
||||
{links.map((link, index) => (
|
||||
<li key={index}>
|
||||
<Link href={link.href} prefetch={false}>
|
||||
<a className={styles.link}>
|
||||
{link.icon} <span>{link.text}</span>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className={styles.theme_toggle}>
|
||||
<ThemeToggle className={styles.icon} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
|
||||
export default memo(Header);
|
||||
@@ -1,3 +0,0 @@
|
||||
import Gist from "react-gist";
|
||||
|
||||
export default Gist;
|
||||
@@ -1,21 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import type { ImageProps } from "next/image";
|
||||
|
||||
const CustomImage = ({ src, width, height, alt, priority }: ImageProps) => {
|
||||
return (
|
||||
<div className="image_wrapper">
|
||||
<Image
|
||||
src={src}
|
||||
layout="intrinsic"
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt || ""}
|
||||
quality={65}
|
||||
loading={priority ? "eager" : "lazy"}
|
||||
priority={!!priority}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomImage;
|
||||
@@ -1,17 +0,0 @@
|
||||
import TweetEmbed from "react-tweet-embed";
|
||||
|
||||
type CustomTweetProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const Tweet = (props: CustomTweetProps) => (
|
||||
<TweetEmbed
|
||||
id={props.id}
|
||||
options={{
|
||||
dnt: true,
|
||||
align: "center",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Tweet;
|
||||
@@ -1,24 +0,0 @@
|
||||
import ReactPlayer from "react-player/lazy";
|
||||
import type { ReactPlayerProps } from "react-player";
|
||||
|
||||
const Video = (props: ReactPlayerProps) => (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
paddingTop: "56.25%",
|
||||
}}
|
||||
>
|
||||
<ReactPlayer
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Video;
|
||||
Reference in New Issue
Block a user