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:
@@ -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")) {
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,10 +33,6 @@
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.markdown_tip a:first-of-type {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hcaptcha {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
14
components/Link/Link.module.css
Normal file
14
components/Link/Link.module.css
Normal 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
49
components/Link/Link.tsx
Normal 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;
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user