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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint