Use Next/Link
This commit is contained in:
parent
35fdc7af14
commit
6abff354ee
|
@ -1,8 +1,7 @@
|
|||
import React, { MouseEventHandler, useCallback } from "react";
|
||||
import { MouseEventHandler, useCallback } from "react";
|
||||
import { Link } from "./Link";
|
||||
import { Ico, Icon } from "components/Ico";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
|
@ -45,10 +44,7 @@ export const Button = ({
|
|||
disabled,
|
||||
size = "normal",
|
||||
}: Props): JSX.Element => (
|
||||
<ConditionalWrapper
|
||||
isWrapping={isDefinedAndNotEmpty(href) && !disabled}
|
||||
wrapperProps={{ href: href ?? "", alwaysNewTab }}
|
||||
wrapper={LinkWrapper}>
|
||||
<Link href={href} alwaysNewTab={alwaysNewTab} disabled={disabled}>
|
||||
<div className="relative">
|
||||
<div
|
||||
draggable={draggable}
|
||||
|
@ -90,7 +86,7 @@ export const Button = ({
|
|||
{isDefinedAndNotEmpty(text) && <p className="-translate-y-[0.05em] text-center">{text}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalWrapper>
|
||||
</Link>
|
||||
);
|
||||
|
||||
/*
|
||||
|
@ -110,19 +106,3 @@ export const TranslatedButton = ({
|
|||
|
||||
return <Button text={selectedTranslation?.text ?? fallback.text} {...otherProps} />;
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface LinkWrapperProps {
|
||||
href: string;
|
||||
alwaysNewTab: boolean;
|
||||
}
|
||||
|
||||
const LinkWrapper = ({ children, alwaysNewTab, href }: LinkWrapperProps & Wrapper) => (
|
||||
<Link href={href} alwaysNewTab={alwaysNewTab}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -1,74 +1,81 @@
|
|||
import router from "next/router";
|
||||
import { MouseEventHandler, useState } from "react";
|
||||
import { isDefined } from "helpers/others";
|
||||
import React, { MouseEventHandler } from "react";
|
||||
import NextLink from "next/link";
|
||||
import { ConditionalWrapper, Wrapper } from "helpers/component";
|
||||
import { isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
href: string | null | undefined;
|
||||
className?: string;
|
||||
allowNewTab?: boolean;
|
||||
alwaysNewTab?: boolean;
|
||||
children: React.ReactNode;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
onFocusChanged?: (isFocused: boolean) => void;
|
||||
disabled?: boolean;
|
||||
linkStyled?: boolean;
|
||||
}
|
||||
|
||||
export const Link = ({
|
||||
href,
|
||||
allowNewTab = true,
|
||||
alwaysNewTab = false,
|
||||
disabled = false,
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
alwaysNewTab,
|
||||
disabled,
|
||||
linkStyled = false,
|
||||
onFocusChanged,
|
||||
}: Props): JSX.Element => {
|
||||
const [isValidClick, setIsValidClick] = useState(false);
|
||||
}: Props): JSX.Element => (
|
||||
<ConditionalWrapper
|
||||
isWrapping={isDefinedAndNotEmpty(href) && !disabled}
|
||||
wrapperProps={{
|
||||
href: href ?? "",
|
||||
alwaysNewTab,
|
||||
onFocusChanged,
|
||||
className: cJoin(
|
||||
cIf(
|
||||
linkStyled,
|
||||
`underline decoration-dark decoration-dotted underline-offset-2 transition-colors
|
||||
hover:text-dark`
|
||||
),
|
||||
className
|
||||
),
|
||||
}}
|
||||
wrapper={LinkWrapper}
|
||||
wrapperFalse={DisabledWrapper}
|
||||
wrapperFalseProps={{ className }}>
|
||||
{children}
|
||||
</ConditionalWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onMouseLeave={() => {
|
||||
setIsValidClick(false);
|
||||
onFocusChanged?.(false);
|
||||
}}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
onMouseDown={(event) => {
|
||||
if (!disabled) {
|
||||
event.preventDefault();
|
||||
onFocusChanged?.(true);
|
||||
setIsValidClick(true);
|
||||
}
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
onFocusChanged?.(false);
|
||||
if (!disabled) {
|
||||
if (isDefined(onClick)) {
|
||||
onClick(event);
|
||||
} else if (isValidClick && href) {
|
||||
if (event.button !== MouseButton.Right) {
|
||||
if (alwaysNewTab) {
|
||||
window.open(href, "_blank", "noopener");
|
||||
} else if (event.button === MouseButton.Left) {
|
||||
if (href.startsWith("#")) {
|
||||
router.replace(href);
|
||||
} else {
|
||||
router.push(href);
|
||||
}
|
||||
} else if (allowNewTab) {
|
||||
window.open(href, "_blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
enum MouseButton {
|
||||
Left = 0,
|
||||
Middle = 1,
|
||||
Right = 2,
|
||||
interface LinkWrapperProps {
|
||||
href: string;
|
||||
className?: string;
|
||||
alwaysNewTab?: boolean;
|
||||
onFocusChanged?: (isFocused: boolean) => void;
|
||||
}
|
||||
|
||||
const LinkWrapper = ({
|
||||
children,
|
||||
className,
|
||||
onFocusChanged,
|
||||
alwaysNewTab = false,
|
||||
href,
|
||||
}: LinkWrapperProps & Wrapper) => (
|
||||
<NextLink
|
||||
href={href}
|
||||
className={className}
|
||||
target={alwaysNewTab ? "_blank" : "_self"}
|
||||
replace={href.startsWith("#")}
|
||||
onMouseLeave={() => onFocusChanged?.(false)}
|
||||
onMouseDown={() => onFocusChanged?.(true)}
|
||||
onMouseUp={() => onFocusChanged?.(false)}>
|
||||
{children}
|
||||
</NextLink>
|
||||
);
|
||||
|
||||
interface DisabledWrapperProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DisabledWrapper = ({ children, className }: DisabledWrapperProps & Wrapper) => (
|
||||
<div className={className}>{children}</div>
|
||||
);
|
||||
|
|
|
@ -34,8 +34,8 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
|
|||
icon={Icon.Favorite}
|
||||
text={expand ? langui.want_it : undefined}
|
||||
active={libraryItemUserStatus[id] === LibraryItemUserStatus.Want}
|
||||
onMouseUp={(event) => event.stopPropagation()}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setLibraryItemUserStatus((current) => {
|
||||
const newLibraryItemUserStatus = { ...current };
|
||||
newLibraryItemUserStatus[id] =
|
||||
|
@ -52,8 +52,8 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
|
|||
icon={Icon.BackHand}
|
||||
text={expand ? langui.have_it : undefined}
|
||||
active={libraryItemUserStatus[id] === LibraryItemUserStatus.Have}
|
||||
onMouseUp={(event) => event.stopPropagation()}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setLibraryItemUserStatus((current) => {
|
||||
const newLibraryItemUserStatus = { ...current };
|
||||
newLibraryItemUserStatus[id] =
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Markdown from "markdown-to-jsx";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Fragment } from "react";
|
||||
import React, { Fragment, useMemo } from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { Img } from "components/Img";
|
||||
|
@ -15,6 +14,7 @@ import { Ico, Icon } from "components/Ico";
|
|||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -30,7 +30,6 @@ interface MarkdawnProps {
|
|||
|
||||
export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Element => {
|
||||
const playerName = useAtomGetter(atoms.settings.playerName);
|
||||
const router = useRouter();
|
||||
const isContentPanelAtLeastLg = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastLg);
|
||||
const { showLightBox } = useAtomGetter(atoms.lightBox);
|
||||
|
||||
|
@ -53,13 +52,15 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
component: (compProps: { href: string; children: React.ReactNode }) => {
|
||||
if (compProps.href.startsWith("/") || compProps.href.startsWith("#")) {
|
||||
return (
|
||||
<a onClick={async () => router.push(compProps.href)}>{compProps.children}</a>
|
||||
<Link href={compProps.href} linkStyled>
|
||||
{compProps.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a href={compProps.href} target="_blank" rel="noreferrer">
|
||||
<Link href={compProps.href} alwaysNewTab linkStyled>
|
||||
{compProps.children}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -95,9 +96,9 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
|
|||
? slugify(compProps.target)
|
||||
: slugify(compProps.children?.toString());
|
||||
return (
|
||||
<a onClick={async () => router.replace(`${compProps.page ?? ""}#${slug}`)}>
|
||||
<Link href={`${compProps.page ?? ""}#${slug}`} linkStyled>
|
||||
{compProps.children}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -224,7 +225,6 @@ export const TableOfContents = ({
|
|||
title,
|
||||
horizontalLine = false,
|
||||
}: TableOfContentsProps): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
|
||||
|
||||
|
@ -238,9 +238,9 @@ export const TableOfContents = ({
|
|||
<p
|
||||
className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap
|
||||
text-left">
|
||||
<a onClick={async () => router.replace(`#${toc.slug}`)}>
|
||||
<Link href={`#${toc.slug}`} linkStyled>
|
||||
{<abbr title={toc.title}>{toc.title}</abbr>}
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
<TocLevel tocchildren={toc.children} parentNumbering="" />
|
||||
</div>
|
||||
|
@ -340,8 +340,7 @@ const TocLevel = ({
|
|||
parentNumbering,
|
||||
allowIntersection = true,
|
||||
}: LevelProps): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const ids = tocchildren.map((child) => child.slug);
|
||||
const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]);
|
||||
const currentIntersection = useIntersectionList(ids);
|
||||
|
||||
return (
|
||||
|
@ -354,9 +353,9 @@ const TocLevel = ({
|
|||
cIf(allowIntersection && currentIntersection === childIndex, "text-dark")
|
||||
)}>
|
||||
<span className="text-dark">{`${parentNumbering}${childIndex + 1}.`}</span>{" "}
|
||||
<a onClick={async () => router.replace(`#${child.slug}`)}>
|
||||
<Link href={`#${child.slug}`} linkStyled>
|
||||
{<abbr title={child.title}>{child.title}</abbr>}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<TocLevel
|
||||
tocchildren={child.children}
|
||||
|
|
|
@ -170,11 +170,12 @@ export const MainPanel = (): JSX.Element => {
|
|||
</p>
|
||||
)}
|
||||
<div className="mt-4 mb-8 grid place-content-center">
|
||||
<a
|
||||
<Link
|
||||
onClick={() => sendAnalytics("MainPanel", "Visit license")}
|
||||
aria-label="Read more about the license we use for this website"
|
||||
className="group grid grid-flow-col place-content-center gap-1 transition-filter"
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/">
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
||||
alwaysNewTab>
|
||||
<ColoredSvg
|
||||
className="h-6 w-6 bg-black group-hover:bg-dark"
|
||||
src="/icons/creative-commons-brands.svg"
|
||||
|
@ -187,7 +188,7 @@ export const MainPanel = (): JSX.Element => {
|
|||
className="h-6 w-6 bg-black group-hover:bg-dark"
|
||||
src="/icons/creative-commons-sa-brands.svg"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
{isDefinedAndNotEmpty(langui.copyright_notice) && (
|
||||
<p>
|
||||
|
@ -195,39 +196,36 @@ export const MainPanel = (): JSX.Element => {
|
|||
</p>
|
||||
)}
|
||||
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">
|
||||
<a
|
||||
<Link
|
||||
aria-label="Browse our GitHub repository, which include this website source code"
|
||||
onClick={() => sendAnalytics("MainPanel", "Visit GitHub")}
|
||||
href="https://github.com/Accords-Library"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
alwaysNewTab>
|
||||
<ColoredSvg
|
||||
className="h-10 w-10 bg-black hover:bg-dark"
|
||||
src="/icons/github-brands.svg"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="Follow us on Twitter"
|
||||
onClick={() => sendAnalytics("MainPanel", "Visit Twitter")}
|
||||
href="https://twitter.com/AccordsLibrary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
alwaysNewTab>
|
||||
<ColoredSvg
|
||||
className="h-10 w-10 bg-black hover:bg-dark"
|
||||
src="/icons/twitter-brands.svg"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="Join our Discord server!"
|
||||
onClick={() => sendAnalytics("MainPanel", "Visit Discord")}
|
||||
href="/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
alwaysNewTab>
|
||||
<ColoredSvg
|
||||
className="h-10 w-10 bg-black hover:bg-dark"
|
||||
src="/icons/discord-brands.svg"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -73,7 +73,7 @@ export const SmartList = <T,>({
|
|||
const [page, setPage] = useState(0);
|
||||
const langui = useAtomGetter(atoms.localData.langui);
|
||||
useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop);
|
||||
useEffect(() => setPage(0), [searchingTerm, groupingFunction, groupSortingFunction, items]);
|
||||
useEffect(() => setPage(0), [searchingTerm, groupingFunction, groupSortingFunction]);
|
||||
|
||||
const searchFilter = useCallback(() => {
|
||||
if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) {
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
import { isDefined } from "./others";
|
||||
|
||||
export interface Wrapper {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ConditionalWrapperProps<T> {
|
||||
interface ConditionalWrapperProps<T, U> {
|
||||
isWrapping: boolean;
|
||||
children: React.ReactNode;
|
||||
wrapper: (wrapperProps: T & Wrapper) => JSX.Element;
|
||||
wrapperProps: T;
|
||||
wrapperFalse?: (wrapperProps: U & Wrapper) => JSX.Element;
|
||||
wrapperFalseProps?: U;
|
||||
}
|
||||
|
||||
export const ConditionalWrapper = <T,>({
|
||||
export const ConditionalWrapper = <T, U>({
|
||||
isWrapping,
|
||||
children,
|
||||
wrapper: Wrapper,
|
||||
wrapperFalse: WrapperFalse,
|
||||
wrapperProps,
|
||||
}: ConditionalWrapperProps<T>): JSX.Element =>
|
||||
isWrapping ? <Wrapper {...wrapperProps}>{children}</Wrapper> : <>{children}</>;
|
||||
wrapperFalseProps,
|
||||
}: ConditionalWrapperProps<T, U>): JSX.Element =>
|
||||
isWrapping ? (
|
||||
<Wrapper {...wrapperProps}>{children}</Wrapper>
|
||||
) : isDefined(WrapperFalse) && isDefined(wrapperFalseProps) ? (
|
||||
<WrapperFalse {...wrapperFalseProps}>{children}</WrapperFalse>
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
|
|
|
@ -36,10 +36,9 @@ const AccordsLibraryApp = (props: AppProps): JSX.Element => {
|
|||
<SettingsPopup />
|
||||
<LightBoxProvider />
|
||||
<Script
|
||||
async
|
||||
defer
|
||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID}
|
||||
src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/umami.js`}
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
<props.Component {...props.pageProps} />
|
||||
</>
|
||||
|
|
|
@ -18,6 +18,7 @@ import { getOpenGraph } from "helpers/openGraph";
|
|||
import { getLangui } from "graphql/fetchLocalData";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
|
@ -95,9 +96,9 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
|
|||
: prettyShortenNumber(video.likes)}
|
||||
</p>
|
||||
)}
|
||||
<a href={`https://youtu.be/${video.uid}`} target="_blank" rel="noreferrer">
|
||||
<Button className="!py-0 !px-3" text={`${langui.view_on} ${video.source}`} />
|
||||
</a>
|
||||
<Link href={`https://youtu.be/${video.uid}`} alwaysNewTab>
|
||||
<Button size="small" text={`${langui.view_on} ${video.source}`} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -53,6 +53,7 @@ import { getLangui } from "graphql/fetchLocalData";
|
|||
import { Ids } from "types/ids";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -713,7 +714,7 @@ const ContentLine = ({
|
|||
cIf(isOpened, "my-2 h-auto bg-mid py-3 shadow-inner-sm shadow-shade")
|
||||
)}>
|
||||
<div className="grid grid-cols-[auto_auto_1fr_auto_12ch] place-items-center gap-4">
|
||||
<a>
|
||||
<Link href={""} linkStyled>
|
||||
<h3 className="cursor-pointer" onClick={toggleOpened}>
|
||||
{selectedTranslation
|
||||
? prettyInlineTitle(
|
||||
|
@ -725,7 +726,7 @@ const ContentLine = ({
|
|||
? prettySlug(content.slug, parentSlug)
|
||||
: prettySlug(slug, parentSlug)}
|
||||
</h3>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
{content?.categories?.map((category, index) => (
|
||||
<Chip key={index} text={category} />
|
||||
|
|
|
@ -19,11 +19,6 @@ h6 {
|
|||
@apply font-headers font-black;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply cursor-pointer underline decoration-dark decoration-dotted
|
||||
underline-offset-2 transition-colors hover:text-dark;
|
||||
}
|
||||
|
||||
*::selection {
|
||||
@apply bg-dark text-light;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue