1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-04-17 09:28:43 -04:00

custom <Link /> wrapper around next/link

This commit is contained in:
2022-01-30 10:33:40 -05:00
parent 2a29d713bb
commit 9f34cec930
35 changed files with 578 additions and 1009 deletions

View File

@@ -1,13 +1,12 @@
import classNames from "classnames";
import CopyButton from "../CopyButton/CopyButton";
import type { ReactNode } from "react";
import type { PropsWithChildren } from "react";
import styles from "./CodeBlock.module.css";
type Props = {
children: ReactNode;
type Props = PropsWithChildren<{
className?: string;
};
}>;
const CodeBlock = ({ children, className, ...rest }: Props) => {
if (className?.split(" ").includes("code-highlight")) {

View File

@@ -1,15 +1,11 @@
import { memo } from "react";
import Link from "next/link";
import type { ReactNode } from "react";
import css from "styled-jsx/css";
import classNames from "classnames";
import Link, { Props as CustomLinkProps } from "../Link/Link";
type Props = {
children: ReactNode;
href: string;
type Props = CustomLinkProps & {
lightColor: string;
darkColor: string;
title?: string;
external?: boolean;
className?: string;
};
// spits out alpha'd version of given color in rgba() format within a linear-gradient (that's not really a gradient)
@@ -25,28 +21,23 @@ const getLinearGradient = (hex: string, alpha = 0.4) => {
return `linear-gradient(${rgbaString},${rgbaString})`;
};
const ColorfulLink = ({ href, lightColor, darkColor, external, className, ...rest }: Props) => {
const ColorfulLink = ({ lightColor, darkColor, className, ...rest }: Props) => {
const { className: underlineClassName, styles: underlineStyles } = css.resolve`
a {
color: ${lightColor};
background-image: ${getLinearGradient(lightColor)};
}
:global([data-theme="dark"]) a {
color: ${darkColor};
background-image: ${getLinearGradient(darkColor)};
}
`;
return (
<>
<Link href={href} passHref={true} prefetch={false}>
<a
className={className}
target={external ? "_blank" : undefined}
rel={external ? "noopener noreferrer" : undefined}
{...rest}
/>
</Link>
<Link className={classNames(underlineClassName, className)} {...rest} />
<style jsx>{`
a {
color: ${lightColor};
background-image: ${getLinearGradient(lightColor)};
}
:global([data-theme="dark"]) a {
color: ${darkColor};
background-image: ${getLinearGradient(darkColor)};
}
`}</style>
{underlineStyles}
</>
);
};

View File

@@ -33,10 +33,6 @@
line-height: 1.75;
}
.markdown_tip a:first-of-type {
font-weight: 500;
}
.hcaptcha {
margin: 1em 0;
}

View File

@@ -3,6 +3,7 @@ import { useTheme } from "next-themes";
import classNames from "classnames/bind";
import { Formik, Form, Field } from "formik";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import Link from "../Link/Link";
import { SendIcon, CheckOcticon, XOcticon } from "../Icons";
import type { FormikHelpers } from "formik";
@@ -126,18 +127,13 @@ const ContactForm = ({ className }: Props) => {
<div className={styles.markdown_tip}>
Basic{" "}
<a
href="https://commonmark.org/help/"
title="Markdown reference sheet"
target="_blank"
rel="noopener noreferrer"
>
<Link href="https://commonmark.org/help/" title="Markdown reference sheet" style={{ fontWeight: 600 }}>
Markdown syntax
</a>{" "}
</Link>{" "}
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
<a href="https://jarv.is" target="_blank" rel="noopener noreferrer">
<Link href="https://jarv.is" forceNewWindow>
links
</a>
</Link>
](https://jarv.is), and <code>`code`</code>.
</div>

View File

@@ -3,28 +3,6 @@
line-height: 1.7;
}
.content a {
color: var(--link);
background-image: linear-gradient(var(--link-underline), var(--link-underline));
background-position: 0% 100%;
background-repeat: no-repeat;
background-size: 0% var(--link-underline-size);
padding-bottom: var(--link-underline-size);
/* background-size is for hover animation, color & border are for theme fading: */
transition: background-size 0.25s ease-in-out, color 0.25s ease, border 0.25s ease;
}
.content a:hover {
background-size: 100% var(--link-underline-size);
}
/* set an anchor's class to `no-underline` to disable all of the above */
.content :global(a.no-underline) {
background: none;
padding-bottom: 0;
transition: none;
}
@media screen and (max-width: 768px) {
.content {
font-size: 0.925em;

View File

@@ -1,12 +1,11 @@
import classNames from "classnames";
import type { ReactNode } from "react";
import type { PropsWithChildren } from "react";
import styles from "./Content.module.css";
type Props = {
children: ReactNode;
type Props = PropsWithChildren<{
className?: string;
};
}>;
const Content = ({ className, ...rest }: Props) => <div className={classNames(styles.content, className)} {...rest} />;

View File

@@ -1,16 +1,16 @@
import Image from "../Image/Image";
import innerText from "react-innertext";
import classNames from "classnames";
import type { ReactNode } from "react";
import type { PropsWithChildren } from "react";
import type { ImageProps as NextImageProps } from "next/image";
import styles from "./Figure.module.css";
type Props = Omit<NextImageProps, "alt"> & {
children: ReactNode;
alt?: string; // becomes optional -- pulled from plaintext-ified caption if missing
className?: string;
};
type Props = Omit<NextImageProps, "alt"> &
PropsWithChildren<{
alt?: string; // becomes optional -- pulled from plaintext-ified caption if missing
className?: string;
}>;
const Figure = ({ children, alt, className, ...imageProps }: Props) => {
return (

View File

@@ -22,9 +22,10 @@
}
.icon {
width: 1.3em;
height: 1.3em;
vertical-align: -0.2em;
width: 1.25em;
height: 1.25em;
vertical-align: -0.25em;
margin: 0 0.075em;
}
.nextjs:hover {

View File

@@ -16,7 +16,7 @@ const Heading = ({ as: Component, id, className, children, ...rest }: Props) =>
{/* add anchor link to H2s and H3s. ID is already generated by rehype-slug. `#` character inserted via CSS. */}
{id && (Component === "h2" || Component === "h3") && (
<a className={classNames("no-underline", styles.anchor)} href={`#${id}`} tabIndex={-1} aria-hidden="true" />
<a className={styles.anchor} href={`#${id}`} tabIndex={-1} aria-hidden="true" />
)}
</Component>
);

View File

@@ -1,17 +1,18 @@
import Head from "next/head";
import { useTheme } from "next-themes";
import classNames from "classnames";
import Header from "../Header/Header";
import Footer from "../Footer/Footer";
import { themeColors } from "../../lib/config";
import type { ReactNode } from "react";
import type { PropsWithChildren } from "react";
import styles from "./Layout.module.css";
type Props = {
children: ReactNode;
};
type Props = PropsWithChildren<{
className?: string;
}>;
const Layout = ({ children }: Props) => {
const Layout = ({ className, children }: Props) => {
const { resolvedTheme } = useTheme();
return (
@@ -23,7 +24,7 @@ const Layout = ({ children }: Props) => {
)}
<Header />
<main className={styles.main}>
<main className={classNames(styles.main, className)}>
<div className={styles.container}>{children}</div>
</main>
<Footer />

View File

@@ -0,0 +1,14 @@
.link {
color: var(--link);
background-image: linear-gradient(var(--link-underline), var(--link-underline));
background-position: 0% 100%;
background-repeat: no-repeat;
background-size: 0% var(--link-underline-size);
padding-bottom: var(--link-underline-size);
/* background-size is for hover animation, color & border are for theme fading: */
transition: background-size 0.25s ease-in-out, color 0.25s ease, border 0.25s ease;
}
.link:hover {
background-size: 100% var(--link-underline-size);
}

49
components/Link/Link.tsx Normal file
View File

@@ -0,0 +1,49 @@
import NextLink from "next/link";
import classNames from "classnames";
import isAbsoluteUrl from "is-absolute-url";
import type { AnchorHTMLAttributes, PropsWithChildren } from "react";
import type { LinkProps } from "next/link";
import styles from "./Link.module.css";
export type Props = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> &
LinkProps &
PropsWithChildren<{
target?: string;
rel?: string;
forceNewWindow?: boolean;
className?: string;
}>;
const CustomLink = ({
href,
prefetch = false,
passHref = true,
target,
rel,
forceNewWindow,
className,
...rest
}: Props) => {
// this component auto-detects whether or not we should use a normal HTML anchor externally or next/link internally,
// can be overridden with `forceNewWindow={true}`.
if (forceNewWindow || isAbsoluteUrl(href.toString())) {
return (
<a
href={href.toString()}
target={target || "_blank"}
rel={rel || "noopener noreferrer"}
className={classNames(styles.link, className)}
{...rest}
/>
);
} else {
return (
<NextLink href={href} prefetch={prefetch} passHref={passHref}>
<a className={classNames(styles.link, className)} {...rest} />
</NextLink>
);
}
};
export default CustomLink;

View File

@@ -1,12 +1,11 @@
import classNames from "classnames";
import type { ReactNode } from "react";
import type { PropsWithChildren } from "react";
import styles from "./List.module.css";
type Props = {
children: ReactNode;
type Props = PropsWithChildren<{
className?: string;
};
}>;
export const UnorderedList = ({ className, ...rest }: Props) => (
<ul className={classNames(styles.unordered, className)} {...rest} />

View File

@@ -1,4 +1,3 @@
import Link from "next/link";
import { format } from "date-fns";
import HitCounter from "../HitCounter/HitCounter";
import { DateIcon, TagIcon, EditIcon, ViewsIcon } from "../Icons";
@@ -15,11 +14,7 @@ const NoteMeta = ({ slug, date, title, tags = [] }: Props) => (
<span>
<DateIcon className={styles.icon} />
</span>
<span title={format(new Date(date), "PPppp")}>
<Link href={`/notes/${slug}/`}>
<a>{format(new Date(date), "MMMM d, yyyy")}</a>
</Link>
</span>
<span title={format(new Date(date), "PPppp")}>{format(new Date(date), "MMMM d, yyyy")}</span>
</div>
{tags.length > 0 && (
@@ -36,19 +31,17 @@ const NoteMeta = ({ slug, date, title, tags = [] }: Props) => (
)}
<div>
<span>
<EditIcon className={styles.icon} />
</span>
<span>
<a
href={`https://github.com/${config.githubRepo}/blob/main/notes/${slug}.mdx`}
target="_blank"
rel="noopener noreferrer"
title={`Edit "${title}" on GitHub`}
>
Improve This Post
</a>
</span>
<a
href={`https://github.com/${config.githubRepo}/blob/main/notes/${slug}.mdx`}
target="_blank"
rel="noopener noreferrer"
title={`Edit "${title}" on GitHub`}
>
<span>
<EditIcon className={styles.icon} />
</span>
<span>Improve This Post</span>
</a>
</div>
<div className={styles.views}>

View File

@@ -5,10 +5,15 @@ import styles from "./NoteTitle.module.css";
type Props = Pick<NoteMetaType, "slug" | "htmlTitle">;
const NoteTitle = ({ slug, htmlTitle }: Props) => (
const NoteTitle = ({ slug, htmlTitle, ...rest }: Props) => (
<h1 className={styles.title}>
<Link href={`/notes/${slug}/`}>
<a dangerouslySetInnerHTML={{ __html: htmlTitle }} />
<Link
href={{
pathname: "/notes/[slug]/",
query: { slug: slug },
}}
>
<a dangerouslySetInnerHTML={{ __html: htmlTitle }} {...rest} />
</Link>
</h1>
);

View File

@@ -1,5 +1,5 @@
import Link from "next/link";
import { format } from "date-fns";
import Link from "../Link/Link";
import type { NoteMetaType } from "../../types";
import styles from "./NotesList.module.css";
@@ -21,10 +21,8 @@ const NotesList = ({ notesByYear }) => {
pathname: "/notes/[slug]/",
query: { slug: slug },
}}
prefetch={false}
>
<a dangerouslySetInnerHTML={{ __html: htmlTitle }} />
</Link>
dangerouslySetInnerHTML={{ __html: htmlTitle }}
/>
</span>
</li>
))}

View File

@@ -8,13 +8,8 @@ type Props = {
className?: string;
};
const OctocatLink = ({ repo, className }: Props) => (
<a
className={classNames("no-underline", styles.link)}
href={`https://github.com/${repo}`}
target="_blank"
rel="noopener noreferrer"
>
const OctocatLink = ({ repo, className, ...rest }: Props) => (
<a className={styles.link} href={`https://github.com/${repo}`} target="_blank" rel="noopener noreferrer" {...rest}>
<OctocatOcticon fill="currentColor" className={classNames(styles.icon, className)} />
</a>
);

View File

@@ -2,23 +2,22 @@ import { useRouter } from "next/router";
import Link from "next/link";
import classNames from "classnames";
import { baseUrl } from "../../lib/config";
import type { ReactNode } from "react";
import type { PropsWithChildren } from "react";
import styles from "./PageTitle.module.css";
type Props = {
children: ReactNode;
type Props = PropsWithChildren<{
className?: string;
};
}>;
const PageTitle = ({ children, className }: Props) => {
const PageTitle = ({ className, ...rest }: Props) => {
const router = useRouter();
const canonical = `${baseUrl}${router.pathname}/`;
return (
<h1 className={classNames(styles.title, className)}>
<Link href={canonical}>
<a>{children}</a>
<a {...rest} />
</Link>
</h1>
);

View File

@@ -1,5 +1,6 @@
import classNames from "classnames";
import { intlFormat, formatDistanceToNowStrict } from "date-fns";
import Link from "../Link/Link";
import { StarOcticon, ForkOcticon } from "../Icons";
import type { RepoType } from "../../types";
@@ -11,9 +12,9 @@ type Props = RepoType & {
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">
<Link className={styles.name} href={url}>
{name}
</a>
</Link>
{description && <p className={styles.description}>{description}</p>}
@@ -28,7 +29,6 @@ const RepositoryCard = ({ name, url, description, language, stars, forks, update
{stars > 0 && (
<div className={styles.meta_item}>
<a
className="no-underline"
href={`${url}/stargazers`}
title={`${stars.toLocaleString("en-US")} ${stars === 1 ? "star" : "stars"}`}
target="_blank"
@@ -43,7 +43,6 @@ const RepositoryCard = ({ name, url, description, language, stars, forks, update
{forks > 0 && (
<div className={styles.meta_item}>
<a
className="no-underline"
href={`${url}/network/members`}
title={`${forks.toLocaleString("en-US")} ${forks === 1 ? "fork" : "forks"}`}
target="_blank"

View File

@@ -1,16 +1,15 @@
import { useEffect, useRef } from "react";
import classNames from "classnames/bind";
import type { ReactNode } from "react";
import type { PropsWithChildren } from "react";
import styles from "./Wallpaper.module.css";
const cx = classNames.bind(styles);
type Props = {
type Props = PropsWithChildren<{
image: string;
tile?: boolean;
className?: string;
children: ReactNode;
};
}>;
const Wallpaper = ({ image, tile, className, ...rest }: Props) => {
const bgRef = useRef<HTMLDivElement>(null);