Use Next/Link

This commit is contained in:
DrMint 2022-12-05 22:01:46 +01:00
parent 35fdc7af14
commit 6abff354ee
11 changed files with 123 additions and 131 deletions

View File

@ -1,8 +1,7 @@
import React, { MouseEventHandler, useCallback } from "react"; import { MouseEventHandler, useCallback } from "react";
import { Link } from "./Link"; import { Link } from "./Link";
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { ConditionalWrapper, Wrapper } from "helpers/component";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others"; import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { TranslatedProps } from "types/TranslatedProps"; import { TranslatedProps } from "types/TranslatedProps";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
@ -45,10 +44,7 @@ export const Button = ({
disabled, disabled,
size = "normal", size = "normal",
}: Props): JSX.Element => ( }: Props): JSX.Element => (
<ConditionalWrapper <Link href={href} alwaysNewTab={alwaysNewTab} disabled={disabled}>
isWrapping={isDefinedAndNotEmpty(href) && !disabled}
wrapperProps={{ href: href ?? "", alwaysNewTab }}
wrapper={LinkWrapper}>
<div className="relative"> <div className="relative">
<div <div
draggable={draggable} draggable={draggable}
@ -90,7 +86,7 @@ export const Button = ({
{isDefinedAndNotEmpty(text) && <p className="-translate-y-[0.05em] text-center">{text}</p>} {isDefinedAndNotEmpty(text) && <p className="-translate-y-[0.05em] text-center">{text}</p>}
</div> </div>
</div> </div>
</ConditionalWrapper> </Link>
); );
/* /*
@ -110,19 +106,3 @@ export const TranslatedButton = ({
return <Button text={selectedTranslation?.text ?? fallback.text} {...otherProps} />; 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>
);

View File

@ -1,74 +1,81 @@
import router from "next/router"; import React, { MouseEventHandler } from "react";
import { MouseEventHandler, useState } from "react"; import NextLink from "next/link";
import { isDefined } from "helpers/others"; import { ConditionalWrapper, Wrapper } from "helpers/component";
import { isDefinedAndNotEmpty } from "helpers/others";
import { cIf, cJoin } from "helpers/className";
interface Props { interface Props {
href: string; href: string | null | undefined;
className?: string; className?: string;
allowNewTab?: boolean;
alwaysNewTab?: boolean; alwaysNewTab?: boolean;
children: React.ReactNode; children: React.ReactNode;
onClick?: MouseEventHandler<HTMLDivElement>; onClick?: MouseEventHandler<HTMLDivElement>;
onFocusChanged?: (isFocused: boolean) => void; onFocusChanged?: (isFocused: boolean) => void;
disabled?: boolean; disabled?: boolean;
linkStyled?: boolean;
} }
export const Link = ({ export const Link = ({
href, href,
allowNewTab = true,
alwaysNewTab = false,
disabled = false,
children, children,
className, className,
onClick, alwaysNewTab,
disabled,
linkStyled = false,
onFocusChanged, onFocusChanged,
}: Props): JSX.Element => { }: Props): JSX.Element => (
const [isValidClick, setIsValidClick] = useState(false); <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 ( interface LinkWrapperProps {
<div href: string;
className={className} className?: string;
onMouseLeave={() => { alwaysNewTab?: boolean;
setIsValidClick(false); onFocusChanged?: (isFocused: boolean) => void;
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,
} }
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>
);

View File

@ -34,8 +34,8 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
icon={Icon.Favorite} icon={Icon.Favorite}
text={expand ? langui.want_it : undefined} text={expand ? langui.want_it : undefined}
active={libraryItemUserStatus[id] === LibraryItemUserStatus.Want} active={libraryItemUserStatus[id] === LibraryItemUserStatus.Want}
onMouseUp={(event) => event.stopPropagation()} onClick={(event) => {
onClick={() => { event.preventDefault();
setLibraryItemUserStatus((current) => { setLibraryItemUserStatus((current) => {
const newLibraryItemUserStatus = { ...current }; const newLibraryItemUserStatus = { ...current };
newLibraryItemUserStatus[id] = newLibraryItemUserStatus[id] =
@ -52,8 +52,8 @@ export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
icon={Icon.BackHand} icon={Icon.BackHand}
text={expand ? langui.have_it : undefined} text={expand ? langui.have_it : undefined}
active={libraryItemUserStatus[id] === LibraryItemUserStatus.Have} active={libraryItemUserStatus[id] === LibraryItemUserStatus.Have}
onMouseUp={(event) => event.stopPropagation()} onClick={(event) => {
onClick={() => { event.preventDefault();
setLibraryItemUserStatus((current) => { setLibraryItemUserStatus((current) => {
const newLibraryItemUserStatus = { ...current }; const newLibraryItemUserStatus = { ...current };
newLibraryItemUserStatus[id] = newLibraryItemUserStatus[id] =

View File

@ -1,6 +1,5 @@
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx";
import { useRouter } from "next/router"; import React, { Fragment, useMemo } from "react";
import React, { Fragment } from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { Img } from "components/Img"; import { Img } from "components/Img";
@ -15,6 +14,7 @@ import { Ico, Icon } from "components/Ico";
import { useDeviceSupportsHover } from "hooks/useMediaQuery"; import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { atoms } from "contexts/atoms"; import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/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 => { export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Element => {
const playerName = useAtomGetter(atoms.settings.playerName); const playerName = useAtomGetter(atoms.settings.playerName);
const router = useRouter();
const isContentPanelAtLeastLg = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastLg); const isContentPanelAtLeastLg = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastLg);
const { showLightBox } = useAtomGetter(atoms.lightBox); 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 }) => { component: (compProps: { href: string; children: React.ReactNode }) => {
if (compProps.href.startsWith("/") || compProps.href.startsWith("#")) { if (compProps.href.startsWith("/") || compProps.href.startsWith("#")) {
return ( return (
<a onClick={async () => router.push(compProps.href)}>{compProps.children}</a> <Link href={compProps.href} linkStyled>
{compProps.children}
</Link>
); );
} }
return ( return (
<a href={compProps.href} target="_blank" rel="noreferrer"> <Link href={compProps.href} alwaysNewTab linkStyled>
{compProps.children} {compProps.children}
</a> </Link>
); );
}, },
}, },
@ -95,9 +96,9 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
? slugify(compProps.target) ? slugify(compProps.target)
: slugify(compProps.children?.toString()); : slugify(compProps.children?.toString());
return ( return (
<a onClick={async () => router.replace(`${compProps.page ?? ""}#${slug}`)}> <Link href={`${compProps.page ?? ""}#${slug}`} linkStyled>
{compProps.children} {compProps.children}
</a> </Link>
); );
}, },
}, },
@ -224,7 +225,6 @@ export const TableOfContents = ({
title, title,
horizontalLine = false, horizontalLine = false,
}: TableOfContentsProps): JSX.Element => { }: TableOfContentsProps): JSX.Element => {
const router = useRouter();
const langui = useAtomGetter(atoms.localData.langui); const langui = useAtomGetter(atoms.localData.langui);
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title); const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
@ -238,9 +238,9 @@ export const TableOfContents = ({
<p <p
className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap
text-left"> text-left">
<a onClick={async () => router.replace(`#${toc.slug}`)}> <Link href={`#${toc.slug}`} linkStyled>
{<abbr title={toc.title}>{toc.title}</abbr>} {<abbr title={toc.title}>{toc.title}</abbr>}
</a> </Link>
</p> </p>
<TocLevel tocchildren={toc.children} parentNumbering="" /> <TocLevel tocchildren={toc.children} parentNumbering="" />
</div> </div>
@ -340,8 +340,7 @@ const TocLevel = ({
parentNumbering, parentNumbering,
allowIntersection = true, allowIntersection = true,
}: LevelProps): JSX.Element => { }: LevelProps): JSX.Element => {
const router = useRouter(); const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]);
const ids = tocchildren.map((child) => child.slug);
const currentIntersection = useIntersectionList(ids); const currentIntersection = useIntersectionList(ids);
return ( return (
@ -354,9 +353,9 @@ const TocLevel = ({
cIf(allowIntersection && currentIntersection === childIndex, "text-dark") cIf(allowIntersection && currentIntersection === childIndex, "text-dark")
)}> )}>
<span className="text-dark">{`${parentNumbering}${childIndex + 1}.`}</span>{" "} <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>} {<abbr title={child.title}>{child.title}</abbr>}
</a> </Link>
</li> </li>
<TocLevel <TocLevel
tocchildren={child.children} tocchildren={child.children}

View File

@ -170,11 +170,12 @@ export const MainPanel = (): JSX.Element => {
</p> </p>
)} )}
<div className="mt-4 mb-8 grid place-content-center"> <div className="mt-4 mb-8 grid place-content-center">
<a <Link
onClick={() => sendAnalytics("MainPanel", "Visit license")} onClick={() => sendAnalytics("MainPanel", "Visit license")}
aria-label="Read more about the license we use for this website" 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" 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 <ColoredSvg
className="h-6 w-6 bg-black group-hover:bg-dark" className="h-6 w-6 bg-black group-hover:bg-dark"
src="/icons/creative-commons-brands.svg" 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" className="h-6 w-6 bg-black group-hover:bg-dark"
src="/icons/creative-commons-sa-brands.svg" src="/icons/creative-commons-sa-brands.svg"
/> />
</a> </Link>
</div> </div>
{isDefinedAndNotEmpty(langui.copyright_notice) && ( {isDefinedAndNotEmpty(langui.copyright_notice) && (
<p> <p>
@ -195,39 +196,36 @@ export const MainPanel = (): JSX.Element => {
</p> </p>
)} )}
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8"> <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" aria-label="Browse our GitHub repository, which include this website source code"
onClick={() => sendAnalytics("MainPanel", "Visit GitHub")} onClick={() => sendAnalytics("MainPanel", "Visit GitHub")}
href="https://github.com/Accords-Library" href="https://github.com/Accords-Library"
target="_blank" alwaysNewTab>
rel="noopener noreferrer">
<ColoredSvg <ColoredSvg
className="h-10 w-10 bg-black hover:bg-dark" className="h-10 w-10 bg-black hover:bg-dark"
src="/icons/github-brands.svg" src="/icons/github-brands.svg"
/> />
</a> </Link>
<a <Link
aria-label="Follow us on Twitter" aria-label="Follow us on Twitter"
onClick={() => sendAnalytics("MainPanel", "Visit Twitter")} onClick={() => sendAnalytics("MainPanel", "Visit Twitter")}
href="https://twitter.com/AccordsLibrary" href="https://twitter.com/AccordsLibrary"
target="_blank" alwaysNewTab>
rel="noopener noreferrer">
<ColoredSvg <ColoredSvg
className="h-10 w-10 bg-black hover:bg-dark" className="h-10 w-10 bg-black hover:bg-dark"
src="/icons/twitter-brands.svg" src="/icons/twitter-brands.svg"
/> />
</a> </Link>
<a <Link
aria-label="Join our Discord server!" aria-label="Join our Discord server!"
onClick={() => sendAnalytics("MainPanel", "Visit Discord")} onClick={() => sendAnalytics("MainPanel", "Visit Discord")}
href="/discord" href="/discord"
target="_blank" alwaysNewTab>
rel="noopener noreferrer">
<ColoredSvg <ColoredSvg
className="h-10 w-10 bg-black hover:bg-dark" className="h-10 w-10 bg-black hover:bg-dark"
src="/icons/discord-brands.svg" src="/icons/discord-brands.svg"
/> />
</a> </Link>
</div> </div>
</div> </div>
</div> </div>

View File

@ -73,7 +73,7 @@ export const SmartList = <T,>({
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const langui = useAtomGetter(atoms.localData.langui); const langui = useAtomGetter(atoms.localData.langui);
useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop); useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop);
useEffect(() => setPage(0), [searchingTerm, groupingFunction, groupSortingFunction, items]); useEffect(() => setPage(0), [searchingTerm, groupingFunction, groupSortingFunction]);
const searchFilter = useCallback(() => { const searchFilter = useCallback(() => {
if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) { if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) {

View File

@ -1,18 +1,30 @@
import { isDefined } from "./others";
export interface Wrapper { export interface Wrapper {
children: React.ReactNode; children: React.ReactNode;
} }
interface ConditionalWrapperProps<T> { interface ConditionalWrapperProps<T, U> {
isWrapping: boolean; isWrapping: boolean;
children: React.ReactNode; children: React.ReactNode;
wrapper: (wrapperProps: T & Wrapper) => JSX.Element; wrapper: (wrapperProps: T & Wrapper) => JSX.Element;
wrapperProps: T; wrapperProps: T;
wrapperFalse?: (wrapperProps: U & Wrapper) => JSX.Element;
wrapperFalseProps?: U;
} }
export const ConditionalWrapper = <T,>({ export const ConditionalWrapper = <T, U>({
isWrapping, isWrapping,
children, children,
wrapper: Wrapper, wrapper: Wrapper,
wrapperFalse: WrapperFalse,
wrapperProps, wrapperProps,
}: ConditionalWrapperProps<T>): JSX.Element => wrapperFalseProps,
isWrapping ? <Wrapper {...wrapperProps}>{children}</Wrapper> : <>{children}</>; }: ConditionalWrapperProps<T, U>): JSX.Element =>
isWrapping ? (
<Wrapper {...wrapperProps}>{children}</Wrapper>
) : isDefined(WrapperFalse) && isDefined(wrapperFalseProps) ? (
<WrapperFalse {...wrapperFalseProps}>{children}</WrapperFalse>
) : (
<>{children}</>
);

View File

@ -36,10 +36,9 @@ const AccordsLibraryApp = (props: AppProps): JSX.Element => {
<SettingsPopup /> <SettingsPopup />
<LightBoxProvider /> <LightBoxProvider />
<Script <Script
async
defer
data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID} data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID}
src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/umami.js`} src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/umami.js`}
strategy="lazyOnload"
/> />
<props.Component {...props.pageProps} /> <props.Component {...props.pageProps} />
</> </>

View File

@ -18,6 +18,7 @@ import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData"; import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms"; import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/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)} : prettyShortenNumber(video.likes)}
</p> </p>
)} )}
<a href={`https://youtu.be/${video.uid}`} target="_blank" rel="noreferrer"> <Link href={`https://youtu.be/${video.uid}`} alwaysNewTab>
<Button className="!py-0 !px-3" text={`${langui.view_on} ${video.source}`} /> <Button size="small" text={`${langui.view_on} ${video.source}`} />
</a> </Link>
</div> </div>
</div> </div>
</div> </div>

View File

@ -53,6 +53,7 @@ import { getLangui } from "graphql/fetchLocalData";
import { Ids } from "types/ids"; import { Ids } from "types/ids";
import { atoms } from "contexts/atoms"; import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/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") 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"> <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}> <h3 className="cursor-pointer" onClick={toggleOpened}>
{selectedTranslation {selectedTranslation
? prettyInlineTitle( ? prettyInlineTitle(
@ -725,7 +726,7 @@ const ContentLine = ({
? prettySlug(content.slug, parentSlug) ? prettySlug(content.slug, parentSlug)
: prettySlug(slug, parentSlug)} : prettySlug(slug, parentSlug)}
</h3> </h3>
</a> </Link>
<div className="flex flex-row flex-wrap gap-1"> <div className="flex flex-row flex-wrap gap-1">
{content?.categories?.map((category, index) => ( {content?.categories?.map((category, index) => (
<Chip key={index} text={category} /> <Chip key={index} text={category} />

View File

@ -19,11 +19,6 @@ h6 {
@apply font-headers font-black; @apply font-headers font-black;
} }
a {
@apply cursor-pointer underline decoration-dark decoration-dotted
underline-offset-2 transition-colors hover:text-dark;
}
*::selection { *::selection {
@apply bg-dark text-light; @apply bg-dark text-light;
} }