1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-07-03 17:26:37 -04:00

move some non-post pages to mdx

This commit is contained in:
2025-03-07 11:53:23 -05:00
parent 8118b8501a
commit 354dade9aa
72 changed files with 811 additions and 1873 deletions

View File

@ -0,0 +1,32 @@
export type CodePenProps = {
username: string;
id: string;
height?: number;
defaultTab?: string;
preview?: boolean;
editable?: boolean;
};
const CodePen = ({
username,
id,
height = 500,
defaultTab = "html",
preview = true,
editable = false,
}: CodePenProps) => {
return (
<iframe
src={`https://codepen.io/${username}/embed/${id}/?${new URLSearchParams({
"default-tab": `${defaultTab},result`,
preview: `${!!preview}`,
editable: `${!!editable}`,
})}`}
scrolling="no"
sandbox="allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation"
style={{ height: `${height}px`, width: "100%", border: "0" }}
/>
);
};
export default CodePen;

View File

@ -0,0 +1,2 @@
export * from "./CodePen";
export { default } from "./CodePen";

View File

@ -1,3 +0,0 @@
.wrapper {
width: 100%;
}

View File

@ -1,41 +0,0 @@
import clsx from "clsx";
import IFrame from "../IFrame";
import styles from "./CodePenEmbed.module.css";
export type CodePenEmbedProps = {
username: string;
id: string;
height?: number;
defaultTab?: string;
preview?: boolean;
editable?: boolean;
className?: string;
};
const CodePenEmbed = ({
username,
id,
height = 500,
defaultTab = "html",
preview = true,
editable = false,
className,
}: CodePenEmbedProps) => {
return (
<div className={clsx(styles.wrapper, className)} style={{ height }}>
<IFrame
src={`https://codepen.io/${username}/embed/${id}/?${new URLSearchParams({
"default-tab": `${defaultTab},result`,
preview: `${!!preview}`,
editable: `${!!editable}`,
})}`}
height={height}
allowScripts
noScroll
/>
</div>
);
};
export default CodePenEmbed;

View File

@ -1,2 +0,0 @@
export * from "./CodePenEmbed";
export { default } from "./CodePenEmbed";

View File

@ -1,6 +0,0 @@
.comments {
margin-top: 2em;
padding-top: 2em;
border-top: 2px solid var(--colors-light);
min-height: 360px;
}

View File

@ -1,44 +1,37 @@
"use client";
import Giscus from "@giscus/react";
import clsx from "clsx";
import useTheme from "../../hooks/useTheme";
import config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react";
import type { GiscusProps } from "@giscus/react";
import styles from "./Comments.module.css";
export type CommentsProps = ComponentPropsWithoutRef<"div"> & {
export type CommentsProps = {
title: string;
};
const Comments = ({ title, className, ...rest }: CommentsProps) => {
const { theme } = useTheme();
const Comments = ({ title }: CommentsProps) => {
// fail silently if giscus isn't configured
if (!config.giscusConfig) {
console.warn("Giscus isn't configured in lib/config/index.js.");
console.warn(
"[giscus] not configured, ensure giscusConfig.repoId and giscusConfig.categoryId are set in lib/config/index.js"
);
return null;
}
// TODO: use custom `<Loading />` spinner component during suspense
return (
<div className={clsx(styles.comments, className)} {...rest}>
<Giscus
repo={config.githubRepo as GiscusProps["repo"]}
repoId={config.giscusConfig.repoId}
term={title}
category="Comments"
categoryId={config.giscusConfig.categoryId}
mapping="specific"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="top"
loading="lazy"
theme={theme === "dark" ? theme : "light"}
/>
</div>
<Giscus
repo={config.githubRepo as GiscusProps["repo"]}
repoId={config.giscusConfig.repoId}
term={title}
category="Comments"
categoryId={config.giscusConfig.categoryId}
mapping="specific"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="top"
loading="lazy"
/>
);
};

43
components/Gist/Gist.tsx Normal file
View File

@ -0,0 +1,43 @@
import Link from "../Link";
export type GistProps = {
id: string;
file?: string;
};
const Gist = async ({ id, file }: GistProps) => {
const iframeId = `gist-${id}${file ? `-${file}` : ""}`;
const scriptUrl = `https://gist.github.com/${id}.js${file ? `?file=${file}` : ""}`;
const scriptResponse = await fetch(scriptUrl);
if (!scriptResponse.ok) {
console.warn(`[gist] fetching js for https://gist.github.com/${id} failed:`, scriptResponse.statusText);
return (
<p style={{ textAlign: "center" }}>
Failed to load gist.{" "}
<Link href={`https://gist.github.com/${id}${file ? `?file=${file}` : ""}`}>Try opening it manually?</Link>
</p>
);
}
const script = await scriptResponse.text();
// https://github.com/tleunen/react-gist/blob/master/src/index.js#L29
const iframeHtml = `<html><head><base target="_parent"></head><body onload="parent.document.getElementById('${iframeId}').style.height=document.body.scrollHeight + 'px'" style="margin:0"><script>${script}</script></body></html>`;
return (
<iframe
width="100%"
scrolling="no"
id={iframeId}
srcDoc={iframeHtml}
sandbox="allow-same-origin allow-scripts allow-popups allow-top-navigation-by-user-activation"
style={{ border: "0", overflow: "hidden" }}
suppressHydrationWarning
></iframe>
);
};
export default Gist;

2
components/Gist/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./Gist";
export { default } from "./Gist";

View File

@ -1,37 +0,0 @@
"use client";
import Frame from "react-frame-component";
import useHasMounted from "../../hooks/useHasMounted";
export type GistEmbedProps = {
id: string;
file?: string;
};
const GistEmbed = ({ id, file }: GistEmbedProps) => {
const hasMounted = useHasMounted();
const scriptUrl = `https://gist.github.com/${id}.js${file ? `?file=${file}` : ""}`;
const iframeId = file ? `gist-${id}-${file}` : `gist-${id}`;
// https://github.com/tleunen/react-gist/blob/master/src/index.js#L29
const iframeHtml = `<html><head><base target="_parent"><style>*{font-size:12px;}</style></head><body onload="parent.document.getElementById('${iframeId}').style.height=document.body.scrollHeight + 'px'" style="margin:0;"><script type="text/javascript" src="${scriptUrl}"></script></body></html>`;
return (
<>
{hasMounted && (
<Frame
width="100%"
frameBorder={0}
scrolling="no"
id={iframeId}
initialContent={iframeHtml}
style={{ height: "0px", overflow: "hidden" }}
>
<></>
</Frame>
)}
</>
);
};
export default GistEmbed;

View File

@ -1,2 +0,0 @@
export * from "./GistEmbed";
export { default } from "./GistEmbed";

View File

@ -1,7 +0,0 @@
.iframe {
width: 100%;
display: block;
margin: 1em auto;
border: 2px solid var(--colors-kindaLight);
border-radius: var(--radii-corner);
}

View File

@ -1,33 +0,0 @@
import clsx from "clsx";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./IFrame.module.css";
export type IFrameProps = ComponentPropsWithoutRef<"iframe"> & {
src: string;
height: number;
width?: number; // defaults to 100%
allowScripts?: boolean;
noScroll?: boolean;
};
const IFrame = ({ src, title, height, width, allowScripts, noScroll, className, style, ...rest }: IFrameProps) => {
return (
<iframe
src={src}
title={title}
sandbox={allowScripts ? "allow-same-origin allow-scripts allow-popups" : undefined}
scrolling={noScroll ? "no" : undefined}
loading="lazy"
className={clsx(styles.iframe, className)}
style={{
height: `${height}px`,
maxWidth: width ? `${width}px` : "100%",
...style,
}}
{...rest}
/>
);
};
export default IFrame;

View File

@ -1,2 +0,0 @@
export * from "./IFrame";
export { default } from "./IFrame";

View File

@ -1,16 +0,0 @@
.flex {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.default {
width: 100%;
padding: 1.5em;
}
.container {
max-width: var(--sizes-maxLayoutWidth);
margin: 0 auto;
display: block;
}

View File

@ -1,30 +0,0 @@
import clsx from "clsx";
import Header from "../Header";
import Footer from "../Footer";
import { SkipToContentLink, SkipToContentTarget } from "../SkipToContent";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Layout.module.css";
export type LayoutProps = ComponentPropsWithoutRef<"div">;
const Layout = ({ className, children, ...rest }: LayoutProps) => {
return (
<>
<SkipToContentLink />
<div className={clsx(styles.flex, className)} {...rest}>
<Header />
<main className={styles.default}>
<SkipToContentTarget />
<div className={styles.container}>{children}</div>
</main>
<Footer />
</div>
</>
);
};
export default Layout;

View File

@ -1,2 +0,0 @@
export * from "./Layout";
export { default } from "./Layout";

View File

@ -2,18 +2,18 @@ import clsx from "clsx";
import Link from "../Link";
import type { Route } from "next";
import type { IconType } from "react-icons";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./MenuItem.module.css";
export type MenuItemProps = {
export type MenuItemProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href"> & {
text?: string;
href?: Route;
icon?: IconType;
current?: boolean;
className?: string;
};
const MenuItem = ({ text, href, icon, current, className }: MenuItemProps) => {
const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemProps) => {
const Icon = icon;
const item = (
@ -32,6 +32,7 @@ const MenuItem = ({ text, href, icon, current, className }: MenuItemProps) => {
title={text}
plain
aria-label={text}
{...rest}
>
{item}
</Link>

View File

@ -1,16 +0,0 @@
.octocatLink {
margin: 0 0.4em;
color: var(--colors-text) !important;
}
.octocatLink:hover,
.octocatLink:focus-visible {
color: var(--colors-link) !important;
}
.octocat {
display: inline;
width: 1.2em;
height: 1.2em;
vertical-align: -0.2em;
}

View File

@ -1,20 +0,0 @@
import Link from "../Link";
import { SiGithub } from "react-icons/si";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./OctocatLink.module.css";
import clsx from "clsx";
export type OctocatLinkProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href"> & {
repo: string;
};
const OctocatLink = ({ repo, className, ...rest }: OctocatLinkProps) => {
return (
<Link href={`https://github.com/${repo}`} plain className={styles.octocatLink} {...rest}>
<SiGithub className={clsx(styles.octocat, className)} />
</Link>
);
};
export default OctocatLink;

View File

@ -1,2 +0,0 @@
export * from "./OctocatLink";
export { default } from "./OctocatLink";

View File

@ -1,22 +1,22 @@
"use client";
import useHasMounted from "../../hooks/useHasMounted";
import { useHasMounted } from "../../hooks";
import { formatDate, formatTimeAgo } from "../../lib/helpers/format-date";
import type { ComponentPropsWithoutRef } from "react";
export type RelativeTimeProps = {
export type RelativeTimeProps = ComponentPropsWithoutRef<"time"> & {
date: string | number | Date;
verb?: string; // optional "Updated", "Published", "Created", etc.
staticFormat?: string; // format for the placeholder/fallback before client-side renders the relative time
className?: string;
};
const RelativeTime = ({ date, verb, staticFormat, className }: RelativeTimeProps) => {
const RelativeTime = ({ date, verb, staticFormat, ...rest }: RelativeTimeProps) => {
// play nice with SSR -- only use relative time on the client, since it'll quickly become outdated on the server and
// cause a react hydration mismatch error.
const hasMounted = useHasMounted();
return (
<time dateTime={formatDate(date)} title={formatDate(date, "MMM D, YYYY, h:mm A z")} className={className}>
<time dateTime={formatDate(date)} title={formatDate(date, "MMM D, YYYY, h:mm A z")} {...rest}>
{verb && `${verb} `}
{hasMounted ? formatTimeAgo(date, { suffix: true }) : `on ${formatDate(date, staticFormat)}`}
</time>

View File

@ -2,9 +2,7 @@
import { useEffect, useId } from "react";
import { animated, Globals, useSpring, useReducedMotion } from "@react-spring/web";
import useFirstMountState from "../../hooks/useFirstMountState";
import useTheme from "../../hooks/useTheme";
import useHasMounted from "../../hooks/useHasMounted";
import { useFirstMountState, useHasMounted, useTheme } from "../../hooks";
import styles from "./ThemeToggle.module.css";

View File

@ -5,14 +5,14 @@ import { getTweet } from "react-tweet/api";
import clsx from "clsx";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./TweetEmbed.module.css";
import styles from "./Tweet.module.css";
export type TweetEmbedProps = Omit<ComponentPropsWithoutRef<typeof EmbeddedTweet>, "tweet"> & {
export type TweetProps = Omit<ComponentPropsWithoutRef<typeof EmbeddedTweet>, "tweet"> & {
id: string;
className?: string;
};
const TweetEmbed = async ({ id, className, ...rest }: TweetEmbedProps) => {
const Tweet = async ({ id, className, ...rest }: TweetProps) => {
try {
const tweet = await getTweet(id);
@ -40,8 +40,12 @@ const TweetEmbed = async ({ id, className, ...rest }: TweetEmbedProps) => {
} catch (
error // eslint-disable-line @typescript-eslint/no-unused-vars
) {
return <TweetNotFound />;
return (
<div className={clsx(styles.tweet, className)}>
<TweetNotFound />
</div>
);
}
};
export default TweetEmbed;
export default Tweet;

View File

@ -0,0 +1,2 @@
export * from "./Tweet";
export { default } from "./Tweet";

View File

@ -1,2 +0,0 @@
export * from "./TweetEmbed";
export { default } from "./TweetEmbed";

View File

@ -4,42 +4,43 @@ import type { ComponentPropsWithoutRef } from "react";
import styles from "./Video.module.css";
export type VideoProps = Omit<Partial<ComponentPropsWithoutRef<"video">>, "src"> & {
src: {
// at least one is required:
webm?: string;
mp4?: string;
// optional:
vtt?: string;
};
src: string[];
poster?: string;
autoplay?: boolean;
responsive?: boolean;
className?: string;
};
const Video = ({ src, poster, autoplay = false, responsive = true, className, ...rest }: VideoProps) => {
if (!src || (!src.mp4 && !src.webm)) {
throw new Error("'src' prop must include either 'mp4' or 'webm' URL.");
}
return (
<div className={clsx(styles.wrapper, responsive && styles.responsive, className)}>
<video
width="100%"
height="100%"
className={styles.player}
preload={autoplay ? "auto" : "metadata"}
controls={!autoplay}
autoPlay={autoplay || undefined}
playsInline={autoplay} // safari autoplay workaround
loop={autoplay || undefined}
muted={autoplay || undefined}
poster={poster}
{...(autoplay
? {
preload: "auto",
controls: false,
autoPlay: true,
playsInline: true, // safari autoplay workaround
loop: true,
muted: true,
}
: {
preload: "metadata",
controls: true,
})}
{...rest}
>
{src.webm && <source key={src.webm} src={src.webm} type="video/webm" />}
{src.mp4 && <source key={src.mp4} src={src.mp4} type="video/mp4" />}
{src.vtt && <track key={src.vtt} kind="subtitles" src={src.vtt} srcLang="en" label="English" default />}
{src.map((file) => {
const extension = file.split(".").pop();
if (extension === "vtt") {
return <track key={file} kind="subtitles" src={file} srcLang="en" label="English" default />;
} else {
return <source key={file} src={file} type={`video/${file.split(".").pop()}`} />;
}
})}
</video>
</div>
);

View File

@ -0,0 +1,3 @@
.wrapper :global(lite-youtube) {
margin: 0 auto;
}

View File

@ -0,0 +1,17 @@
import { YouTubeEmbed } from "@next/third-parties/google";
import styles from "./YouTube.module.css";
export type YouTubeProps = {
id: string;
};
const YouTube = ({ id }: YouTubeProps) => {
return (
<div className={styles.wrapper}>
<YouTubeEmbed videoid={id} />
</div>
);
};
export default YouTube;

View File

@ -0,0 +1,2 @@
export * from "./YouTube";
export { default } from "./YouTube";

View File

@ -1,15 +0,0 @@
.wrapper {
position: relative;
padding-top: 56.25%; /* ratio of 1280x720 */
}
.player {
position: absolute;
top: 0;
left: 0;
}
.player .react-player__preview,
.player iframe {
border-radius: var(--radii-corner);
}

View File

@ -1,27 +0,0 @@
import clsx from "clsx";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./YouTubeEmbed.module.css";
export type YouTubeEmbedProps = ComponentPropsWithoutRef<"div"> & {
id: string;
};
const YouTubeEmbed = ({ id, className, ...rest }: YouTubeEmbedProps) => {
return (
<div className={clsx(styles.wrapper, className)} {...rest}>
<iframe
src={`https://www.youtube-nocookie.com/embed/${id}`}
className={styles.player}
width="100%"
height="100%"
frameBorder="0"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
);
};
export default YouTubeEmbed;

View File

@ -1,2 +0,0 @@
export * from "./YouTubeEmbed";
export { default } from "./YouTubeEmbed";