1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-06-30 22:46:39 -04:00

all components should accept additional classnames

This commit is contained in:
2022-01-20 12:06:05 -05:00
parent 2162e9d563
commit 7e37adabc1
22 changed files with 150 additions and 86 deletions

View File

@ -1,11 +1,12 @@
import classNames from "classnames";
import type { BlockquoteHTMLAttributes } from "react";
import styles from "./Blockquote.module.css";
type Props = BlockquoteHTMLAttributes<HTMLElement>;
const Blockquote = ({ children, ...rest }: Props) => (
<blockquote className={styles.blockquote} {...rest}>
const Blockquote = ({ children, className, ...rest }: Props) => (
<blockquote className={classNames(styles.blockquote, className)} {...rest}>
{children}
</blockquote>
);

View File

@ -5,24 +5,28 @@ import type { ReactNode } from "react";
import styles from "./CodeBlock.module.css";
type Props = {
className?: string;
children: ReactNode;
className?: string;
};
const CodeBlock = (props: Props) => {
if (props.className?.split(" ").includes("code-highlight")) {
const CodeBlock = ({ children, className, ...rest }: Props) => {
if (className?.split(" ").includes("code-highlight")) {
// full multi-line code blocks with prism highlighting and copy-to-clipboard button
return (
<div className={styles.block}>
<CopyButton source={props.children} className={styles.copy_btn} />
<code {...props} className={classNames(styles.code, props.className)}>
{props.children}
<CopyButton source={children} className={styles.copy_btn} />
<code className={classNames(styles.code, className)} {...rest}>
{children}
</code>
</div>
);
} else {
// inline code in paragraphs, headings, etc. (not highlighted)
return <code className={classNames(styles.code, styles.inline)}>{props.children}</code>;
return (
<code className={classNames(styles.code, styles.inline, className)} {...rest}>
{children}
</code>
);
}
};

View File

@ -1,6 +1,7 @@
import { memo } from "react";
import Link from "next/link";
import css from "styled-jsx/css";
import classNames from "classnames";
import type { ReactNode } from "react";
type Props = {
@ -10,6 +11,7 @@ type Props = {
darkColor: string;
title?: string;
external?: boolean;
className?: string;
};
const getFancyLinkStyles = ({ lightColor, darkColor }: Partial<Props>) => {
@ -39,23 +41,33 @@ const getFancyLinkStyles = ({ lightColor, darkColor }: Partial<Props>) => {
`;
};
const ColorfulLink = ({ href, title, lightColor, darkColor, external = false, children }: Props) => {
const { className, styles } = getFancyLinkStyles({ lightColor, darkColor });
const ColorfulLink = ({
href,
title,
lightColor,
darkColor,
external = false,
className,
children,
...rest
}: Props) => {
const { className: underlineClassName, styles: underlineStyles } = getFancyLinkStyles({ lightColor, darkColor });
return (
<>
<Link href={href} passHref={true} prefetch={false}>
<a
className={className}
className={classNames(underlineClassName, className)}
title={title}
target={external ? "_blank" : undefined}
rel={external ? "noopener noreferrer" : undefined}
{...rest}
>
{children}
</a>
</Link>
{styles}
{underlineStyles}
</>
);
};

View File

@ -1,4 +1,5 @@
import { useTheme } from "next-themes";
import classNames from "classnames";
import { Giscus } from "@giscus/react";
import { giscusConfig } from "../../lib/config";
import type { GiscusProps } from "@giscus/react";
@ -7,13 +8,14 @@ import styles from "./Comments.module.css";
type Props = {
title: string;
className?: string;
};
const Comments = ({ title }: Props) => {
const Comments = ({ title, className }: Props) => {
const { resolvedTheme } = useTheme();
return (
<div className={styles.wrapper}>
<div className={classNames(styles.wrapper, className)}>
<Giscus
{...(giscusConfig as GiscusProps)}
term={title}

View File

@ -11,6 +11,10 @@ import type { FormikHelpers } from "formik";
import styles from "./ContactForm.module.css";
const cx = classNames.bind(styles);
type Props = {
className?: string;
};
type Values = {
name: string;
email: string;
@ -18,7 +22,7 @@ type Values = {
"h-captcha-response": string;
};
const ContactForm = () => {
const ContactForm = ({ className }: Props) => {
const { resolvedTheme } = useTheme();
// status/feedback:
@ -97,7 +101,7 @@ const ContactForm = () => {
}}
>
{({ setFieldValue, isSubmitting, touched, errors }) => (
<Form className={styles.form} name="contact">
<Form className={className} name="contact">
<Field
className={cx({ input: true, missing: errors.name && touched.name })}
name="name"

View File

@ -50,11 +50,7 @@ const CopyButton = ({ source, timeout = 2000, className }: Props) => {
onClick={handleCopy}
disabled={!!copied}
>
{copied ? (
<CheckOcticon className="icon" fill="currentColor" />
) : (
<ClipboardOcticon className="icon" fill="currentColor" />
)}
{copied ? <CheckOcticon fill="currentColor" /> : <ClipboardOcticon fill="currentColor" />}
</button>
);
};

View File

@ -1,5 +1,6 @@
import Image from "../Image/Image";
import innerText from "react-innertext";
import classNames from "classnames";
import type { ReactNode } from "react";
import type { ImageProps as NextImageProps } from "next/image";
@ -8,11 +9,12 @@ import styles from "./Figure.module.css";
type Props = Omit<NextImageProps, "alt"> & {
children: ReactNode; // caption (can be in markdown, yay!!!)
alt?: string; // becomes optional -- pulled from plaintext-ified caption if missing
className?: string;
};
const Figure = ({ children, alt, ...imageProps }: Props) => {
const Figure = ({ children, alt, className, ...imageProps }: Props) => {
return (
<figure className={styles.figure}>
<figure className={classNames(styles.figure, className)}>
<Image alt={alt || innerText(children)} {...imageProps} />
<figcaption className={styles.caption}>{children}</figcaption>
</figure>

View File

@ -5,44 +5,45 @@ import styles from "./Heading.module.css";
type Props = HTMLAttributes<HTMLHeadingElement> & {
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
className?: string;
};
const Heading = ({ as: Component, children, ...rest }: Props) => {
const Heading = ({ as: Component, children, className, ...rest }: Props) => {
return (
<Component className={classNames(styles.heading, styles[Component])} {...rest}>
<Component className={classNames(styles.heading, styles[Component], className)} {...rest}>
{children}
</Component>
);
};
// TODO: do this less manually...
export const H1 = ({ children, ...props }: Props) => (
<Heading as="h1" {...props}>
export const H1 = ({ children, ...rest }: Props) => (
<Heading as="h1" {...rest}>
{children}
</Heading>
);
export const H2 = ({ children, ...props }: Props) => (
<Heading as="h2" {...props}>
export const H2 = ({ children, ...rest }: Props) => (
<Heading as="h2" {...rest}>
{children}
</Heading>
);
export const H3 = ({ children, ...props }: Props) => (
<Heading as="h3" {...props}>
export const H3 = ({ children, ...rest }: Props) => (
<Heading as="h3" {...rest}>
{children}
</Heading>
);
export const H4 = ({ children, ...props }: Props) => (
<Heading as="h4" {...props}>
export const H4 = ({ children, ...rest }: Props) => (
<Heading as="h4" {...rest}>
{children}
</Heading>
);
export const H5 = ({ children, ...props }: Props) => (
<Heading as="h5" {...props}>
export const H5 = ({ children, ...rest }: Props) => (
<Heading as="h5" {...rest}>
{children}
</Heading>
);
export const H6 = ({ children, ...props }: Props) => (
<Heading as="h6" {...props}>
export const H6 = ({ children, ...rest }: Props) => (
<Heading as="h6" {...rest}>
{children}
</Heading>
);

View File

@ -2,7 +2,12 @@ import useSWR from "swr";
import Loading from "../Loading/Loading";
import { fetcher } from "../../lib/fetcher";
const HitCounter = ({ slug }) => {
type Props = {
slug: string;
className?: string;
};
const HitCounter = ({ slug, className }: Props) => {
// start fetching repos from API immediately
const { data, error } = useSWR(`/api/hits/?slug=${encodeURIComponent(slug)}`, fetcher, {
// avoid double (or more) counting views
@ -21,7 +26,7 @@ const HitCounter = ({ slug }) => {
// we have data!
return (
<span title={`${data.hits.toLocaleString("en-US")} ${data.hits === 1 ? "view" : "views"}`}>
<span title={`${data.hits.toLocaleString("en-US")} ${data.hits === 1 ? "view" : "views"}`} className={className}>
{data.hits.toLocaleString("en-US")}
</span>
);

View File

@ -1,5 +1,11 @@
import classNames from "classnames";
import styles from "./HorizontalRule.module.css";
const HorizontalRule = () => <hr className={styles.hr} />;
type Props = {
className?: string;
};
const HorizontalRule = ({ className, ...rest }: Props) => <hr className={classNames(styles.hr, className)} {...rest} />;
export default HorizontalRule;

View File

@ -1,3 +1,5 @@
import classNames from "classnames";
import styles from "./IFrame.module.css";
type Props = {
@ -7,11 +9,12 @@ type Props = {
width?: number; // defaults to 100%
allowScripts?: boolean;
noScroll?: boolean;
className?: string;
};
const IFrame = ({ src, title, height, width, allowScripts, noScroll, ...rest }: Props) => (
const IFrame = ({ src, title, height, width, allowScripts, noScroll, className, ...rest }: Props) => (
<iframe
className={styles.frame}
className={classNames(styles.frame, className)}
src={src}
title={title}
sandbox={allowScripts ? "allow-same-origin allow-scripts allow-popups" : undefined}

View File

@ -1,11 +1,12 @@
import NextImage from "next/image";
import classNames from "classnames";
import type { ImageProps as NextImageProps } from "next/image";
import styles from "./Image.module.css";
const Image = ({ src, width, height, alt, quality, priority }: NextImageProps) => {
const Image = ({ src, width, height, alt, quality, priority, className, ...rest }: NextImageProps) => {
return (
<div className={styles.wrapper}>
<div className={classNames(styles.wrapper, className)}>
<NextImage
src={(src as string).replace(/^\/public/g, "")}
layout="intrinsic"
@ -15,6 +16,7 @@ const Image = ({ src, width, height, alt, quality, priority }: NextImageProps) =
quality={quality || 65}
loading={priority ? "eager" : "lazy"}
priority={!!priority}
{...rest}
/>
</div>
);

View File

@ -1,25 +1,27 @@
import classNames from "classnames";
import type { ReactNode } from "react";
import styles from "./List.module.css";
type Props = {
children: ReactNode;
className?: string;
};
export const UnorderedList = ({ children, ...rest }: Props) => (
<ul className={styles.unordered} {...rest}>
export const UnorderedList = ({ children, className, ...rest }: Props) => (
<ul className={classNames(styles.unordered, className)} {...rest}>
{children}
</ul>
);
export const OrderedList = ({ children, ...rest }: Props) => (
<ol className={styles.ordered} {...rest}>
export const OrderedList = ({ children, className, ...rest }: Props) => (
<ol className={classNames(styles.ordered, className)} {...rest}>
{children}
</ol>
);
// TODO: this is based on good faith that the children are all `<li>`s...
export const ListItem = ({ children, ...rest }: Props) => (
<li className={styles.item} {...rest}>
export const ListItem = ({ children, className, ...rest }: Props) => (
<li className={classNames(styles.item, className)} {...rest}>
{children}
</li>
);

View File

@ -1,4 +1,5 @@
import { memo } from "react";
import classNames from "classnames";
import styles from "./Loading.module.css";
@ -6,9 +7,10 @@ type Props = {
width: number; // of entire container, in pixels
boxes?: number; // total number of boxes (default: 3)
timing?: number; // staggered timing between each box's pulse, in seconds (default: 0.1s)
className?: string;
};
const Loading = ({ width, boxes = 3, timing = 0.1 }: Props) => {
const Loading = ({ width, boxes = 3, timing = 0.1, className }: Props) => {
// each box is just an empty div
const divs = [];
@ -30,7 +32,7 @@ const Loading = ({ width, boxes = 3, timing = 0.1 }: Props) => {
return (
<div
className={styles.wrapper}
className={classNames(styles.wrapper, className)}
style={{
width: `${width}px`,
height: `${width / 2}px`,

View File

@ -1,14 +1,16 @@
import classNames from "classnames";
import { OctocatOcticon } from "../Icons";
import styles from "./OctocatLink.module.css";
type Props = {
repo: string;
className?: string;
};
const OctocatLink = (props: Props) => (
<a className={styles.link} href={`https://github.com/${props.repo}`} target="_blank" rel="noopener noreferrer">
<OctocatOcticon fill="currentColor" />
const OctocatLink = ({ repo, className }: Props) => (
<a className={styles.link} href={`https://github.com/${repo}`} target="_blank" rel="noopener noreferrer">
<OctocatOcticon fill="currentColor" className={classNames("icon", className)} />
</a>
);

View File

@ -1,19 +1,21 @@
import { memo } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import classNames from "classnames";
import type { ReactNode } from "react";
import styles from "./PageTitle.module.css";
type Props = {
children: ReactNode;
className?: string;
};
const PageTitle = ({ children }: Props) => {
const PageTitle = ({ children, className }: Props) => {
const router = useRouter();
return (
<h1 className={styles.title}>
<h1 className={classNames(styles.title, className)}>
<Link href={router.asPath}>
<a>{children}</a>
</Link>

View File

@ -1,49 +1,54 @@
import classNames from "classnames";
import { intlFormat, formatDistanceToNowStrict } from "date-fns";
import { StarOcticon, ForkOcticon } from "../Icons";
import { RepoType } from "../../types";
import styles from "./RepositoryCard.module.css";
const RepositoryCard = (props: RepoType) => (
<div className={styles.card}>
<a className={styles.name} href={props.url} target="_blank" rel="noopener noreferrer">
{props.name}
type Props = RepoType & {
className?: string;
};
const RepositoryCard = ({ name, url, description, language, stars, forks, updatedAt, className }: Props) => (
<div className={classNames(styles.card, className)}>
<a className={styles.name} href={url} target="_blank" rel="noopener noreferrer">
{name}
</a>
{props.description && <p className={styles.description}>{props.description}</p>}
{description && <p className={styles.description}>{description}</p>}
<div className={styles.meta}>
{props.language && (
{language && (
<div className={styles.meta_item}>
<span className={styles.language_color} style={{ backgroundColor: props.language.color }} />
<span>{props.language.name}</span>
<span className={styles.language_color} style={{ backgroundColor: language.color }} />
<span>{language.name}</span>
</div>
)}
{props.stars > 0 && (
{stars > 0 && (
<div className={styles.meta_item}>
<a
href={`${props.url}/stargazers`}
title={`${props.stars.toLocaleString("en-US")} ${props.stars === 1 ? "star" : "stars"}`}
href={`${url}/stargazers`}
title={`${stars.toLocaleString("en-US")} ${stars === 1 ? "star" : "stars"}`}
target="_blank"
rel="noopener noreferrer"
>
<StarOcticon fill="currentColor" className={styles.octicon} />
<span>{props.stars.toLocaleString("en-US")}</span>
<span>{stars.toLocaleString("en-US")}</span>
</a>
</div>
)}
{props.forks > 0 && (
{forks > 0 && (
<div className={styles.meta_item}>
<a
href={`${props.url}/network/members`}
title={`${props.forks.toLocaleString("en-US")} ${props.forks === 1 ? "fork" : "forks"}`}
href={`${url}/network/members`}
title={`${forks.toLocaleString("en-US")} ${forks === 1 ? "fork" : "forks"}`}
target="_blank"
rel="noopener noreferrer"
>
<ForkOcticon fill="currentColor" className={styles.octicon} />
<span>{props.forks.toLocaleString("en-US")}</span>
<span>{forks.toLocaleString("en-US")}</span>
</a>
</div>
)}
@ -51,7 +56,7 @@ const RepositoryCard = (props: RepoType) => (
<div
className={styles.meta_item}
title={intlFormat(
new Date(props.updatedAt),
new Date(updatedAt),
{
year: "numeric",
month: "short",
@ -65,7 +70,7 @@ const RepositoryCard = (props: RepoType) => (
}
)}
>
<span>Updated {formatDistanceToNowStrict(new Date(props.updatedAt), { addSuffix: true })}</span>
<span>Updated {formatDistanceToNowStrict(new Date(updatedAt), { addSuffix: true })}</span>
</div>
</div>
</div>

View File

@ -2,14 +2,18 @@ import Tweet from "react-tweet-embed";
type Props = {
id: string;
className?: string;
options?: object;
};
const TweetEmbed = (props: Props) => (
const TweetEmbed = ({ id, className, options }: Props) => (
<Tweet
id={props.id}
className={className}
id={id}
options={{
dnt: true,
align: "center",
...options,
}}
/>
);

View File

@ -1,3 +1,4 @@
import classNames from "classnames";
import ReactPlayer from "react-player/file";
import styles from "./Video.module.css";
@ -8,9 +9,10 @@ type Props = {
thumbnail?: string;
subs?: string;
autoplay?: boolean;
className?: string;
};
const Video = ({ webm, mp4, thumbnail, subs, autoplay }: Props) => {
const Video = ({ webm, mp4, thumbnail, subs, autoplay, className, ...rest }: Props) => {
const url = [
webm && {
src: webm,
@ -53,9 +55,9 @@ const Video = ({ webm, mp4, thumbnail, subs, autoplay }: Props) => {
}
return (
<div className={styles.wrapper}>
<div className={classNames(styles.wrapper, className)}>
{/* @ts-ignore */}
<ReactPlayer width="100%" height="100%" url={url} config={config} controls={!autoplay} />
<ReactPlayer width="100%" height="100%" url={url} config={config} controls={!autoplay} {...rest} />
</div>
);
};

View File

@ -1,19 +1,22 @@
import classNames from "classnames";
import ReactPlayer from "react-player/youtube";
import styles from "./YouTubeEmbed.module.css";
type Props = {
id: string;
className?: string;
};
const YouTubeEmbed = ({ id }: Props) => (
<div className={styles.wrapper}>
const YouTubeEmbed = ({ id, className, ...rest }: Props) => (
<div className={classNames(styles.wrapper, className)}>
<ReactPlayer
width="100%"
height="100%"
url={`https://www.youtube-nocookie.com/watch?v=${id}`}
light={`https://i.ytimg.com/vi/${id}/hqdefault.jpg`}
controls
{...rest}
/>
</div>
);

View File

@ -259,7 +259,7 @@ const Index = () => (
</ColorfulLink>{" "}
<sup className="monospace pgp_key">
<ColorfulLink href="/pubkey.asc" title="My Public Key" lightColor="#757575" darkColor="#959595" external>
<LockIcon className="icon" /> 2B0C 9CF2 51E6 9A39
<LockIcon /> 2B0C 9CF2 51E6 9A39
</ColorfulLink>
</sup>
,{" "}

View File

@ -6,7 +6,11 @@ import { ProjectsIcon } from "../components/Icons";
import type { GetStaticProps } from "next";
import { RepoType } from "../types";
const Projects = (props: { repos: RepoType[] }) => (
type Props = {
repos: RepoType[];
};
const Projects = ({ repos }: Props) => (
<>
<NextSeo
title="Projects"
@ -20,7 +24,7 @@ const Projects = (props: { repos: RepoType[] }) => (
</PageTitle>
<div className="wrapper">
{props.repos.map((repo: RepoType) => (
{repos.map((repo: RepoType) => (
<div key={repo.name} className="card">
<RepositoryCard {...repo} />
</div>