1
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:
2022-01-18 09:25:09 -05:00
committed by GitHub
parent 2ef5d06c38
commit a406010bd2
110 changed files with 1009 additions and 1490 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
import GistEmbed from "react-gist";
export default GistEmbed;

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

View 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);

View File

@@ -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 }) => {

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

View File

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

View 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
View 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);

View 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
View 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);

View File

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

View File

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

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

View File

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

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

View File

@@ -0,0 +1,10 @@
.wrapper {
position: relative;
padding-top: 56.25%;
}
.wrapper > div {
position: absolute;
top: 0;
left: 0;
}

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

View File

@@ -0,0 +1,10 @@
.wrapper {
position: relative;
padding-top: 56.25%;
}
.wrapper > div {
position: absolute;
top: 0;
left: 0;
}

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
import Gist from "react-gist";
export default Gist;

View File

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

View File

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

View File

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