Lots of bs
This commit is contained in:
		
							parent
							
								
									5a963294b7
								
							
						
					
					
						commit
						625f436163
					
				| @ -77,4 +77,9 @@ interface ComponentProps {} | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ | ||||
| 
 | ||||
| export const Component = () => {}; | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  TRANSLATED VARIANT  ╰────────────────────────────────────── | ||||
|  */ | ||||
| ``` | ||||
|  | ||||
| @ -4,7 +4,7 @@ const locales = ["en", "es", "fr", "pt-br", "ja"]; | ||||
| 
 | ||||
| /* END CONFIG */ | ||||
| 
 | ||||
| /** @type {import('next').NextConfig} */ | ||||
| /* @type {import('next').NextConfig} */ | ||||
| module.exports = { | ||||
|   swcMinify: true, | ||||
|   reactStrictMode: true, | ||||
| @ -15,9 +15,6 @@ module.exports = { | ||||
|   images: { | ||||
|     domains: ["img.accords-library.com", "watch.accords-library.com"], | ||||
|   }, | ||||
|   serverRuntimeConfig: { | ||||
|     locales: locales, | ||||
|   }, | ||||
|   async redirects() { | ||||
|     return [ | ||||
|       { | ||||
|  | ||||
| @ -3,12 +3,12 @@ import { useRouter } from "next/router"; | ||||
| import { useEffect, useLayoutEffect, useMemo, useState } from "react"; | ||||
| import { useSwipeable } from "react-swipeable"; | ||||
| import UAParser from "ua-parser-js"; | ||||
| import { useBoolean, useIsClient } from "usehooks-ts"; | ||||
| import { Ico, Icon } from "./Ico"; | ||||
| import { ButtonGroup } from "./Inputs/ButtonGroup"; | ||||
| import { OrderableList } from "./Inputs/OrderableList"; | ||||
| import { Select } from "./Inputs/Select"; | ||||
| import { TextInput } from "./Inputs/TextInput"; | ||||
| import { ContentPlaceholder } from "./PanelComponents/ContentPlaceholder"; | ||||
| import { MainPanel } from "./Panels/MainPanel"; | ||||
| import { Popup } from "./Popup"; | ||||
| import { AnchorIds } from "hooks/useScrollTopOnChange"; | ||||
| @ -27,8 +27,6 @@ import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { OpenGraph, TITLE_PREFIX } from "helpers/openGraph"; | ||||
| import { getDefaultPreferredLanguages } from "helpers/locales"; | ||||
| import useIsClient from "hooks/useIsClient"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| 
 | ||||
| /* | ||||
|  *                                         ╭─────────────╮ | ||||
| @ -195,7 +193,7 @@ export const AppLayout = ({ | ||||
|   }, [mainPanelReduced, subPanel]); | ||||
| 
 | ||||
|   const isClient = useIsClient(); | ||||
|   const { state: hasDisgardSafariWarning, setTrue: disgardSafariWarning } = | ||||
|   const { value: hasDisgardSafariWarning, setTrue: disgardSafariWarning } = | ||||
|     useBoolean(false); | ||||
|   const isSafari = useMemo<boolean>(() => { | ||||
|     if (isClient) { | ||||
| @ -548,3 +546,29 @@ export const AppLayout = ({ | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| interface ContentPlaceholderProps { | ||||
|   message: string; | ||||
|   icon?: Icon; | ||||
| } | ||||
| 
 | ||||
| const ContentPlaceholder = ({ | ||||
|   message, | ||||
|   icon, | ||||
| }: ContentPlaceholderProps): JSX.Element => ( | ||||
|   <div className="grid h-full place-content-center"> | ||||
|     <div | ||||
|       className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted | ||||
|         border-dark p-8 text-dark opacity-40" | ||||
|     > | ||||
|       {isDefined(icon) && <Ico icon={icon} className="!text-[300%]" />} | ||||
|       <p | ||||
|         className={cJoin("w-64 text-2xl", cIf(!isDefined(icon), "text-center"))} | ||||
|       > | ||||
|         {message} | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
|  | ||||
| @ -1,6 +1,9 @@ | ||||
| import { useCallback } from "react"; | ||||
| import { Link } from "components/Inputs/Link"; | ||||
| import { DatePickerFragment } from "graphql/generated"; | ||||
| import { cIf, cJoin } from "helpers/className"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -14,7 +17,7 @@ interface Props { | ||||
|   isActive?: boolean; | ||||
| } | ||||
| 
 | ||||
| export const ChroniclePreview = ({ | ||||
| const ChroniclePreview = ({ | ||||
|   date, | ||||
|   url, | ||||
|   title, | ||||
| @ -40,6 +43,35 @@ export const ChroniclePreview = ({ | ||||
|   </Link> | ||||
| ); | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  TRANSLATED VARIANT  ╰────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| export const TranslatedChroniclePreview = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps< | ||||
|   Parameters<typeof ChroniclePreview>[0], | ||||
|   "title" | ||||
| >): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <ChroniclePreview | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
|  *                                      ╭───────────────────╮ | ||||
|  * ─────────────────────────────────────╯  PRIVATE METHODS  ╰─────────────────────────────────────── | ||||
|  | ||||
| @ -1,10 +1,13 @@ | ||||
| import { useCallback } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { TranslatedChroniclePreview } from "./ChroniclePreview"; | ||||
| import { GetChroniclesChaptersQuery } from "graphql/generated"; | ||||
| import { filterHasAttributes } from "helpers/others"; | ||||
| import { TranslatedChroniclePreview } from "components/Translated"; | ||||
| import { prettyInlineTitle, prettySlug } from "helpers/formatters"; | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| import { compareDate } from "helpers/date"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -23,12 +26,12 @@ interface Props { | ||||
|   title: string; | ||||
| } | ||||
| 
 | ||||
| export const ChroniclesList = ({ | ||||
| const ChroniclesList = ({ | ||||
|   chronicles, | ||||
|   currentSlug, | ||||
|   title, | ||||
| }: Props): JSX.Element => { | ||||
|   const { state: isOpen, toggleState: toggleOpen } = useBoolean( | ||||
|   const { value: isOpen, toggle: toggleOpen } = useBoolean( | ||||
|     chronicles.some((chronicle) => chronicle.attributes?.slug === currentSlug) | ||||
|   ); | ||||
| 
 | ||||
| @ -112,3 +115,29 @@ export const ChroniclesList = ({ | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  TRANSLATED VARIANT  ╰────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| export const TranslatedChroniclesList = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<Props, "title">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <ChroniclesList | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -1,9 +1,11 @@ | ||||
| import React, { MouseEventHandler } from "react"; | ||||
| import React, { 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 "helpers/types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -89,6 +91,29 @@ export const Button = ({ | ||||
|   </ConditionalWrapper> | ||||
| ); | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  TRANSLATED VARIANT  ╰────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| export const TranslatedButton = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<Props, "text">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Button text={selectedTranslation?.text ?? fallback.text} {...otherProps} /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  PRIVATE COMPONENTS  ╰────────────────────────────────────── | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { Fragment, useCallback } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { cIf, cJoin } from "helpers/className"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -27,9 +27,9 @@ export const Select = ({ | ||||
|   onChange, | ||||
| }: Props): JSX.Element => { | ||||
|   const { | ||||
|     state: isOpened, | ||||
|     value: isOpened, | ||||
|     setFalse: setClosed, | ||||
|     toggleState: toggleOpened, | ||||
|     toggle: toggleOpened, | ||||
|   } = useBoolean(false); | ||||
| 
 | ||||
|   const tryToggling = useCallback(() => { | ||||
| @ -39,6 +39,9 @@ export const Select = ({ | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       onClickCapture={() => { | ||||
|         setClosed(); | ||||
|       }} | ||||
|       className={cJoin( | ||||
|         "relative text-center transition-[filter]", | ||||
|         cIf(isOpened, "z-10 drop-shadow-shade-lg"), | ||||
|  | ||||
| @ -8,13 +8,17 @@ import { isDefinedAndNotEmpty } from "helpers/others"; | ||||
| 
 | ||||
| interface Props { | ||||
|   label: string | null | undefined; | ||||
|   input: JSX.Element; | ||||
|   disabled?: boolean; | ||||
|   children: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const WithLabel = ({ label, input, disabled }: Props): JSX.Element => ( | ||||
| export const WithLabel = ({ | ||||
|   label, | ||||
|   children, | ||||
|   disabled, | ||||
| }: Props): JSX.Element => ( | ||||
|   <div | ||||
|     className={cJoin( | ||||
|       "flex flex-row place-content-between place-items-center gap-2", | ||||
| @ -28,6 +32,6 @@ export const WithLabel = ({ label, input, disabled }: Props): JSX.Element => ( | ||||
|         {label}: | ||||
|       </p> | ||||
|     )} | ||||
|     {input} | ||||
|     {children} | ||||
|   </div> | ||||
| ); | ||||
|  | ||||
| @ -39,7 +39,7 @@ export const PreviewCardCTAs = ({ | ||||
|             onClick={(event) => { | ||||
|               event.preventDefault(); | ||||
|               setLibraryItemUserStatus((current) => { | ||||
|                 const newLibraryItemUserStatus = current ? { ...current } : {}; | ||||
|                 const newLibraryItemUserStatus = { ...current }; | ||||
|                 newLibraryItemUserStatus[id] = | ||||
|                   newLibraryItemUserStatus[id] === LibraryItemUserStatus.Want | ||||
|                     ? LibraryItemUserStatus.None | ||||
| @ -57,7 +57,7 @@ export const PreviewCardCTAs = ({ | ||||
|             onClick={(event) => { | ||||
|               event.preventDefault(); | ||||
|               setLibraryItemUserStatus((current) => { | ||||
|                 const newLibraryItemUserStatus = current ? { ...current } : {}; | ||||
|                 const newLibraryItemUserStatus = { ...current }; | ||||
|                 newLibraryItemUserStatus[id] = | ||||
|                   newLibraryItemUserStatus[id] === LibraryItemUserStatus.Have | ||||
|                     ? LibraryItemUserStatus.None | ||||
|  | ||||
| @ -1,246 +0,0 @@ | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Img } from "components/Img"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { RecorderChip } from "components/RecorderChip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { GetLibraryItemScansQuery } from "graphql/generated"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { isInteger } from "helpers/numbers"; | ||||
| import { | ||||
|   filterHasAttributes, | ||||
|   getStatusDescription, | ||||
|   isDefined, | ||||
|   isDefinedAndNotEmpty, | ||||
| } from "helpers/others"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
|  * ───────────────────────────────────────╯  COMPONENT  ╰─────────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| interface Props { | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
|   scanSet: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         NonNullable< | ||||
|           NonNullable< | ||||
|             GetLibraryItemScansQuery["libraryItems"] | ||||
|           >["data"][number]["attributes"] | ||||
|         >["contents"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["scan_set"] | ||||
|   >; | ||||
|   id: string; | ||||
|   title: string; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   langui: AppStaticProps["langui"]; | ||||
|   content: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         NonNullable< | ||||
|           GetLibraryItemScansQuery["libraryItems"] | ||||
|         >["data"][number]["attributes"] | ||||
|       >["contents"] | ||||
|     >["data"][number]["attributes"] | ||||
|   >["content"]; | ||||
| } | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const ScanSet = ({ | ||||
|   openLightBox, | ||||
|   scanSet, | ||||
|   id, | ||||
|   title, | ||||
|   languages, | ||||
|   langui, | ||||
|   content, | ||||
| }: Props): JSX.Element => { | ||||
|   const [selectedScan, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: scanSet, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<Props["scanSet"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|       transform: useCallback((item: NonNullable<Props["scanSet"][number]>) => { | ||||
|         item.pages?.data.sort((a, b) => { | ||||
|           if ( | ||||
|             a.attributes && | ||||
|             b.attributes && | ||||
|             isDefinedAndNotEmpty(a.attributes.url) && | ||||
|             isDefinedAndNotEmpty(b.attributes.url) | ||||
|           ) { | ||||
|             let aName = getAssetFilename(a.attributes.url); | ||||
|             let bName = getAssetFilename(b.attributes.url); | ||||
| 
 | ||||
|             /* | ||||
|              * If the number is a succession of 0s, make the number | ||||
|              * incrementally smaller than 0 (i.e: 00 becomes -1) | ||||
|              */ | ||||
|             if (aName.replaceAll("0", "").length === 0) { | ||||
|               aName = (1 - aName.length).toString(10); | ||||
|             } | ||||
|             if (bName.replaceAll("0", "").length === 0) { | ||||
|               bName = (1 - bName.length).toString(10); | ||||
|             } | ||||
| 
 | ||||
|             if (isInteger(aName) && isInteger(bName)) { | ||||
|               return parseInt(aName, 10) - parseInt(bName, 10); | ||||
|             } | ||||
|             return a.attributes.url.localeCompare(b.attributes.url); | ||||
|           } | ||||
|           return 0; | ||||
|         }); | ||||
|         return item; | ||||
|       }, []), | ||||
|     }); | ||||
| 
 | ||||
|   const pages = useMemo( | ||||
|     () => filterHasAttributes(selectedScan?.pages?.data, ["attributes"]), | ||||
|     [selectedScan] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {selectedScan && isDefined(pages) && ( | ||||
|         <div> | ||||
|           <div | ||||
|             className="flex flex-row flex-wrap place-items-center | ||||
|           gap-6 pt-10 text-base first-of-type:pt-0" | ||||
|           > | ||||
|             <h2 id={id} className="text-2xl"> | ||||
|               {title} | ||||
|             </h2> | ||||
| 
 | ||||
|             <Chip | ||||
|               text={ | ||||
|                 selectedScan.language?.data?.attributes?.code === | ||||
|                 selectedScan.source_language?.data?.attributes?.code | ||||
|                   ? langui.scan ?? "Scan" | ||||
|                   : langui.scanlation ?? "Scanlation" | ||||
|               } | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex flex-row flex-wrap place-items-center gap-4 pb-6"> | ||||
|             {content?.data?.attributes && | ||||
|               isDefinedAndNotEmpty(content.data.attributes.slug) && ( | ||||
|                 <Button | ||||
|                   href={`/contents/${content.data.attributes.slug}`} | ||||
|                   text={langui.open_content} | ||||
|                 /> | ||||
|               )} | ||||
| 
 | ||||
|             {languageSwitcherProps.locales.size > 1 && ( | ||||
|               <LanguageSwitcher {...languageSwitcherProps} /> | ||||
|             )} | ||||
| 
 | ||||
|             <div className="grid place-content-center place-items-center"> | ||||
|               <p className="font-headers font-bold">{langui.status}:</p> | ||||
|               <ToolTip | ||||
|                 content={getStatusDescription(selectedScan.status, langui)} | ||||
|                 maxWidth={"20rem"} | ||||
|               > | ||||
|                 <Chip text={selectedScan.status} /> | ||||
|               </ToolTip> | ||||
|             </div> | ||||
| 
 | ||||
|             {selectedScan.scanners && selectedScan.scanners.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{langui.scanners}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(selectedScan.scanners.data, [ | ||||
|                     "id", | ||||
|                     "attributes", | ||||
|                   ] as const).map((scanner) => ( | ||||
|                     <Fragment key={scanner.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={scanner.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{langui.cleaners}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(selectedScan.cleaners.data, [ | ||||
|                     "id", | ||||
|                     "attributes", | ||||
|                   ] as const).map((cleaner) => ( | ||||
|                     <Fragment key={cleaner.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={cleaner.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {selectedScan.typesetters && | ||||
|               selectedScan.typesetters.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold"> | ||||
|                     {langui.typesetters}: | ||||
|                   </p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(selectedScan.typesetters.data, [ | ||||
|                       "id", | ||||
|                       "attributes", | ||||
|                     ] as const).map((typesetter) => ( | ||||
|                       <Fragment key={typesetter.id}> | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={typesetter.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|             {isDefinedAndNotEmpty(selectedScan.notes) && ( | ||||
|               <ToolTip content={selectedScan.notes}> | ||||
|                 <Chip text={langui.notes ?? "Notes"} /> | ||||
|               </ToolTip> | ||||
|             )} | ||||
|           </div> | ||||
| 
 | ||||
|           <div | ||||
|             className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 | ||||
|              desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] mobile:grid-cols-2" | ||||
|           > | ||||
|             {pages.map((page, index) => ( | ||||
|               <div | ||||
|                 key={page.id} | ||||
|                 className="cursor-pointer transition-transform | ||||
|                 drop-shadow-shade-lg hover:scale-[1.02]" | ||||
|                 onClick={() => { | ||||
|                   const images = pages.map((image) => | ||||
|                     getAssetURL(image.attributes.url, ImageQuality.Large) | ||||
|                   ); | ||||
|                   openLightBox(images, index); | ||||
|                 }} | ||||
|               > | ||||
|                 <Img src={page.attributes} quality={ImageQuality.Small} /> | ||||
|               </div> | ||||
|             ))} | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @ -1,191 +0,0 @@ | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Img } from "components/Img"; | ||||
| import { RecorderChip } from "components/RecorderChip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { | ||||
|   GetLibraryItemScansQuery, | ||||
|   UploadImageFragment, | ||||
| } from "graphql/generated"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { filterHasAttributes, getStatusDescription } from "helpers/others"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
|  * ───────────────────────────────────────╯  COMPONENT  ╰─────────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| interface Props { | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
|   images: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         GetLibraryItemScansQuery["libraryItems"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["images"] | ||||
|   >; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const ScanSetCover = ({ | ||||
|   openLightBox, | ||||
|   images, | ||||
|   languages, | ||||
|   langui, | ||||
| }: Props): JSX.Element => { | ||||
|   const [selectedScan, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: images, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<Props["images"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|   const coverImages = useMemo(() => { | ||||
|     const memo: UploadImageFragment[] = []; | ||||
|     if (selectedScan?.obi_belt?.full?.data?.attributes) | ||||
|       memo.push(selectedScan.obi_belt.full.data.attributes); | ||||
|     if (selectedScan?.obi_belt?.inside_full?.data?.attributes) | ||||
|       memo.push(selectedScan.obi_belt.inside_full.data.attributes); | ||||
|     if (selectedScan?.dust_jacket?.full?.data?.attributes) | ||||
|       memo.push(selectedScan.dust_jacket.full.data.attributes); | ||||
|     if (selectedScan?.dust_jacket?.inside_full?.data?.attributes) | ||||
|       memo.push(selectedScan.dust_jacket.inside_full.data.attributes); | ||||
|     if (selectedScan?.cover?.full?.data?.attributes) | ||||
|       memo.push(selectedScan.cover.full.data.attributes); | ||||
|     if (selectedScan?.cover?.inside_full?.data?.attributes) | ||||
|       memo.push(selectedScan.cover.inside_full.data.attributes); | ||||
|     return memo; | ||||
|   }, [selectedScan]); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {coverImages.length > 0 && selectedScan && ( | ||||
|         <div> | ||||
|           <div | ||||
|             className="flex flex-row flex-wrap place-items-center | ||||
|           gap-6 pt-10 text-base first-of-type:pt-0" | ||||
|           > | ||||
|             <h2 id={"cover"} className="text-2xl"> | ||||
|               {langui.cover} | ||||
|             </h2> | ||||
| 
 | ||||
|             <Chip | ||||
|               text={ | ||||
|                 selectedScan.language?.data?.attributes?.code === | ||||
|                 selectedScan.source_language?.data?.attributes?.code | ||||
|                   ? langui.scan ?? "Scan" | ||||
|                   : langui.scanlation ?? "Scanlation" | ||||
|               } | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex flex-row flex-wrap place-items-center gap-4 pb-6"> | ||||
|             <LanguageSwitcher {...languageSwitcherProps} /> | ||||
| 
 | ||||
|             <div className="grid place-content-center place-items-center"> | ||||
|               <p className="font-headers font-bold">{langui.status}:</p> | ||||
|               <ToolTip | ||||
|                 content={getStatusDescription(selectedScan.status, langui)} | ||||
|                 maxWidth={"20rem"} | ||||
|               > | ||||
|                 <Chip text={selectedScan.status} /> | ||||
|               </ToolTip> | ||||
|             </div> | ||||
| 
 | ||||
|             {selectedScan.scanners && selectedScan.scanners.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{langui.scanners}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(selectedScan.scanners.data, [ | ||||
|                     "id", | ||||
|                     "attributes", | ||||
|                   ] as const).map((scanner) => ( | ||||
|                     <Fragment key={scanner.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={scanner.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{langui.cleaners}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(selectedScan.cleaners.data, [ | ||||
|                     "id", | ||||
|                     "attributes", | ||||
|                   ] as const).map((cleaner) => ( | ||||
|                     <Fragment key={cleaner.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={cleaner.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {selectedScan.typesetters && | ||||
|               selectedScan.typesetters.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold"> | ||||
|                     {langui.typesetters}: | ||||
|                   </p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(selectedScan.typesetters.data, [ | ||||
|                       "id", | ||||
|                       "attributes", | ||||
|                     ] as const).map((typesetter) => ( | ||||
|                       <Fragment key={typesetter.id}> | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={typesetter.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
|           </div> | ||||
| 
 | ||||
|           <div | ||||
|             className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 | ||||
|               last-of-type:border-0 desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] | ||||
|               mobile:grid-cols-2" | ||||
|           > | ||||
|             {coverImages.map((image, index) => ( | ||||
|               <div | ||||
|                 key={image.url} | ||||
|                 className="cursor-pointer transition-transform | ||||
|                   drop-shadow-shade-lg hover:scale-[1.02]" | ||||
|                 onClick={() => { | ||||
|                   const imgs = coverImages.map((img) => | ||||
|                     getAssetURL(img.url, ImageQuality.Large) | ||||
|                   ); | ||||
| 
 | ||||
|                   openLightBox(imgs, index); | ||||
|                 }} | ||||
|               > | ||||
|                 <Img src={image} quality={ImageQuality.Small} /> | ||||
|               </div> | ||||
|             ))} | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @ -1,31 +0,0 @@ | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { cIf, cJoin } from "helpers/className"; | ||||
| import { isDefined } from "helpers/others"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
|  * ───────────────────────────────────────╯  COMPONENT  ╰─────────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| interface Props { | ||||
|   message: string; | ||||
|   icon?: Icon; | ||||
| } | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const ContentPlaceholder = ({ message, icon }: Props): JSX.Element => ( | ||||
|   <div className="grid h-full place-content-center"> | ||||
|     <div | ||||
|       className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted | ||||
|         border-dark p-8 text-dark opacity-40" | ||||
|     > | ||||
|       {isDefined(icon) && <Ico icon={icon} className="!text-[300%]" />} | ||||
|       <p | ||||
|         className={cJoin("w-64 text-2xl", cIf(!isDefined(icon), "text-center"))} | ||||
|       > | ||||
|         {message} | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
| @ -1,10 +1,12 @@ | ||||
| import { useRouter } from "next/router"; | ||||
| import { MouseEventHandler, useMemo } from "react"; | ||||
| import { MouseEventHandler, useCallback, useMemo } from "react"; | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { cJoin, cIf } from "helpers/className"; | ||||
| import { isDefinedAndNotEmpty } from "helpers/others"; | ||||
| import { Link } from "components/Inputs/Link"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -81,3 +83,29 @@ export const NavOption = ({ | ||||
|     </ToolTip> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  TRANSLATED VARIANT  ╰────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| export const TranslatedNavOption = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<Props, "subtitle" | "title">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
|   return ( | ||||
|     <NavOption | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       subtitle={selectedTranslation?.subtitle ?? fallback.subtitle} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -1,9 +1,12 @@ | ||||
| import { useCallback } from "react"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Icon } from "components/Ico"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { cJoin } from "helpers/className"; | ||||
| import { cIf, cJoin } from "helpers/className"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -40,11 +43,8 @@ export const ReturnButton = ({ | ||||
|   return ( | ||||
|     <div | ||||
|       className={cJoin( | ||||
|         displayOn === ReturnButtonType.Mobile | ||||
|           ? "desktop:hidden" | ||||
|           : displayOn === ReturnButtonType.Desktop | ||||
|           ? "mobile:hidden" | ||||
|           : "", | ||||
|         cIf(displayOn === ReturnButtonType.Mobile, "desktop:hidden"), | ||||
|         cIf(displayOn === ReturnButtonType.Desktop, "mobile:hidden"), | ||||
|         className | ||||
|       )} | ||||
|     > | ||||
| @ -58,3 +58,29 @@ export const ReturnButton = ({ | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  TRANSLATED VARIANT  ╰────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| export const TranslatedReturnButton = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<Props, "title">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <ReturnButton | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { useMemo } from "react"; | ||||
| import { useCallback, useMemo } from "react"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { Chip } from "./Chip"; | ||||
| import { Ico, Icon } from "./Ico"; | ||||
| @ -20,6 +20,8 @@ import { | ||||
| } from "helpers/formatters"; | ||||
| import { ImageQuality } from "helpers/img"; | ||||
| import { useMediaHoverable } from "hooks/useMediaQuery"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -296,3 +298,34 @@ export const PreviewCard = ({ | ||||
|     </Link> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  TRANSLATED VARIANT  ╰────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| export const TranslatedPreviewCard = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps< | ||||
|   Props, | ||||
|   "description" | "pre_title" | "subtitle" | "title" | ||||
| >): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
|   return ( | ||||
|     <PreviewCard | ||||
|       pre_title={selectedTranslation?.pre_title ?? fallback.pre_title} | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       subtitle={selectedTranslation?.subtitle ?? fallback.subtitle} | ||||
|       description={selectedTranslation?.description ?? fallback.description} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -1,8 +1,11 @@ | ||||
| import { useCallback } from "react"; | ||||
| import { Chip } from "./Chip"; | ||||
| import { Img } from "./Img"; | ||||
| import { Link } from "./Inputs/Link"; | ||||
| import { UploadImageFragment } from "graphql/generated"; | ||||
| import { ImageQuality } from "helpers/img"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -22,7 +25,7 @@ interface Props { | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const PreviewLine = ({ | ||||
| const PreviewLine = ({ | ||||
|   href, | ||||
|   thumbnail, | ||||
|   pre_title, | ||||
| @ -73,3 +76,30 @@ export const PreviewLine = ({ | ||||
|     </div> | ||||
|   </Link> | ||||
| ); | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  TRANSLATED VARIANT  ╰────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| export const TranslatedPreviewLine = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<Props, "pre_title" | "subtitle" | "title">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
|   return ( | ||||
|     <PreviewLine | ||||
|       pre_title={selectedTranslation?.pre_title ?? fallback.pre_title} | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       subtitle={selectedTranslation?.subtitle ?? fallback.subtitle} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -1,226 +0,0 @@ | ||||
| import { PreviewCard } from "./PreviewCard"; | ||||
| import { PreviewLine } from "./PreviewLine"; | ||||
| import { ScanSet } from "./Library/ScanSet"; | ||||
| import { NavOption } from "./PanelComponents/NavOption"; | ||||
| import { ChroniclePreview } from "./Chronicles/ChroniclePreview"; | ||||
| import { ChroniclesList } from "./Chronicles/ChroniclesList"; | ||||
| import { Button } from "./Inputs/Button"; | ||||
| import { ReturnButton } from "./PanelComponents/ReturnButton"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { PreviewFolder } from "pages/contents/folder/[slug]"; | ||||
| 
 | ||||
| export type TranslatedProps<P, K extends keyof P> = Omit<P, K> & { | ||||
|   translations: (Pick<P, K> & { language: string })[]; | ||||
|   fallback: Pick<P, K>; | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| type TranslatedPreviewCardProps = TranslatedProps< | ||||
|   Parameters<typeof PreviewCard>[0], | ||||
|   "description" | "pre_title" | "subtitle" | "title" | ||||
| >; | ||||
| 
 | ||||
| const languageExtractor = (item: { language: string }): string => item.language; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedPreviewCard = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedPreviewCardProps): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <PreviewCard | ||||
|       pre_title={selectedTranslation?.pre_title ?? fallback.pre_title} | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       subtitle={selectedTranslation?.subtitle ?? fallback.subtitle} | ||||
|       description={selectedTranslation?.description ?? fallback.description} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedPreviewLine = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps< | ||||
|   Parameters<typeof PreviewLine>[0], | ||||
|   "pre_title" | "subtitle" | "title" | ||||
| >): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
| 
 | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <PreviewLine | ||||
|       pre_title={selectedTranslation?.pre_title ?? fallback.pre_title} | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       subtitle={selectedTranslation?.subtitle ?? fallback.subtitle} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedScanSet = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<Parameters<typeof ScanSet>[0], "title">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <ScanSet | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedNavOption = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps< | ||||
|   Parameters<typeof NavOption>[0], | ||||
|   "subtitle" | "title" | ||||
| >): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <NavOption | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       subtitle={selectedTranslation?.subtitle ?? fallback.subtitle} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedChroniclePreview = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps< | ||||
|   Parameters<typeof ChroniclePreview>[0], | ||||
|   "title" | ||||
| >): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <ChroniclePreview | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedChroniclesList = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps< | ||||
|   Parameters<typeof ChroniclesList>[0], | ||||
|   "title" | ||||
| >): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <ChroniclesList | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedButton = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<Parameters<typeof Button>[0], "text">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Button text={selectedTranslation?.text ?? fallback.text} {...otherProps} /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedPreviewFolder = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps< | ||||
|   Parameters<typeof PreviewFolder>[0], | ||||
|   "title" | ||||
| >): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <PreviewFolder | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedReturnButton = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps< | ||||
|   Parameters<typeof ReturnButton>[0], | ||||
|   "title" | ||||
| >): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <ReturnButton | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @ -2,7 +2,7 @@ export interface Wrapper { | ||||
|   children: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| interface Props<T> { | ||||
| interface ConditionalWrapperProps<T> { | ||||
|   isWrapping: boolean; | ||||
|   children: React.ReactNode; | ||||
|   wrapper: (wrapperProps: T & Wrapper) => JSX.Element; | ||||
| @ -14,7 +14,7 @@ export const ConditionalWrapper = <T,>({ | ||||
|   children, | ||||
|   wrapper: Wrapper, | ||||
|   wrapperProps, | ||||
| }: Props<T>): JSX.Element => | ||||
| }: ConditionalWrapperProps<T>): JSX.Element => | ||||
|   isWrapping ? ( | ||||
|     <Wrapper {...wrapperProps}>{children}</Wrapper> | ||||
|   ) : ( | ||||
|  | ||||
| @ -24,13 +24,3 @@ export const randomInt = (min: number, max: number): number => | ||||
| 
 | ||||
| export const isInteger = (value: string): boolean => | ||||
|   /^[+-]?[0-9]+$/u.test(value); | ||||
| 
 | ||||
| export const clamp = ( | ||||
|   value: number, | ||||
|   minValue: number, | ||||
|   maxValue: number | ||||
| ): number => { | ||||
|   if (value > maxValue) return maxValue; | ||||
|   if (value < minValue) return minValue; | ||||
|   return value; | ||||
| }; | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| /* eslint-disable @typescript-eslint/no-unused-vars */ | ||||
| 
 | ||||
| type JoinDot<K extends string, P extends string> = `${K}${"" extends K | ||||
|   ? "" | ||||
|   : "."}${P}`;
 | ||||
|  | ||||
							
								
								
									
										4
									
								
								src/helpers/types/TranslatedProps.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/helpers/types/TranslatedProps.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| export type TranslatedProps<P, K extends keyof P> = Omit<P, K> & { | ||||
|   translations: (Pick<P, K> & { language: string })[]; | ||||
|   fallback: Pick<P, K>; | ||||
| }; | ||||
| @ -1,20 +0,0 @@ | ||||
| import { Dispatch, SetStateAction, useCallback, useState } from "react"; | ||||
| 
 | ||||
| export const useBoolean = ( | ||||
|   initialState: boolean | ||||
| ): { | ||||
|   state: boolean; | ||||
|   toggleState: () => void; | ||||
|   setTrue: () => void; | ||||
|   setFalse: () => void; | ||||
|   setState: Dispatch<SetStateAction<boolean>>; | ||||
| } => { | ||||
|   const [state, setState] = useState(initialState); | ||||
|   const toggleState = useCallback( | ||||
|     () => setState((currentState) => !currentState), | ||||
|     [] | ||||
|   ); | ||||
|   const setTrue = useCallback(() => setState(true), []); | ||||
|   const setFalse = useCallback(() => setState(false), []); | ||||
|   return { state, toggleState, setTrue, setFalse, setState }; | ||||
| }; | ||||
| @ -1,13 +0,0 @@ | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| const useIsClient = (): boolean => { | ||||
|   const [isClient, setClient] = useState(false); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setClient(true); | ||||
|   }, []); | ||||
| 
 | ||||
|   return isClient; | ||||
| }; | ||||
| 
 | ||||
| export default useIsClient; | ||||
| @ -1,38 +1,6 @@ | ||||
| import { useCallback, useEffect, useState } from "react"; | ||||
| import { useMediaQuery } from "usehooks-ts"; | ||||
| import { breaks } from "../../design.config"; | ||||
| 
 | ||||
| const useMediaQuery = (query: string): boolean => { | ||||
|   const getMatches = useCallback((): boolean => { | ||||
|     // Prevents SSR issues
 | ||||
|     if (typeof window !== "undefined") { | ||||
|       return window.matchMedia(query).matches; | ||||
|     } | ||||
|     return false; | ||||
|   }, [query]); | ||||
| 
 | ||||
|   const [matches, setMatches] = useState<boolean>(getMatches()); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const handleChange = () => { | ||||
|       setMatches(getMatches()); | ||||
|     }; | ||||
| 
 | ||||
|     const matchMedia = window.matchMedia(query); | ||||
| 
 | ||||
|     // Triggered at the first client-side load and if query changes
 | ||||
|     handleChange(); | ||||
| 
 | ||||
|     // Listen matchMedia
 | ||||
|     matchMedia.addEventListener("change", handleChange); | ||||
| 
 | ||||
|     return () => { | ||||
|       matchMedia.removeEventListener("change", handleChange); | ||||
|     }; | ||||
|   }, [getMatches, query]); | ||||
| 
 | ||||
|   return matches; | ||||
| }; | ||||
| 
 | ||||
| // ts-unused-exports:disable-next-line
 | ||||
| export const useMediaThin = (): boolean => useMediaQuery(breaks.thin.raw); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										38
									
								
								src/hooks/useTernaryDarkMode.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/hooks/useTernaryDarkMode.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| import { useMemo } from "react"; | ||||
| import { usePrefersDarkMode } from "./useMediaQuery"; | ||||
| import { useStateWithLocalStorage } from "./useStateWithLocalStorage"; | ||||
| 
 | ||||
| export enum TernaryDarkMode { | ||||
|   Dark = "dark", | ||||
|   Auto = "auto", | ||||
|   Light = "light", | ||||
| } | ||||
| 
 | ||||
| export const useTernaryDarkMode = ( | ||||
|   key: string, | ||||
|   initialValue: TernaryDarkMode | ||||
| ): { | ||||
|   isDarkMode: boolean; | ||||
|   ternaryDarkMode: TernaryDarkMode | undefined; | ||||
|   setTernaryDarkMode: React.Dispatch< | ||||
|     React.SetStateAction<TernaryDarkMode | undefined> | ||||
|   >; | ||||
| } => { | ||||
|   const [ternaryDarkMode, setTernaryDarkMode] = useStateWithLocalStorage( | ||||
|     key, | ||||
|     initialValue | ||||
|   ); | ||||
|   const prefersDarkMode = usePrefersDarkMode(); | ||||
|   const isDarkMode = useMemo(() => { | ||||
|     switch (ternaryDarkMode) { | ||||
|       case TernaryDarkMode.Light: | ||||
|         return false; | ||||
|       case TernaryDarkMode.Dark: | ||||
|         return true; | ||||
|       default: | ||||
|         return prefersDarkMode; | ||||
|     } | ||||
|   }, [prefersDarkMode, ternaryDarkMode]); | ||||
| 
 | ||||
|   return { isDarkMode, ternaryDarkMode, setTernaryDarkMode }; | ||||
| }; | ||||
| @ -1,5 +1,5 @@ | ||||
| import type { NextApiRequest, NextApiResponse } from "next"; | ||||
| import getConfig from "next/config"; | ||||
| import { i18n } from "../../../next.config"; | ||||
| 
 | ||||
| type RequestProps = | ||||
|   | HookChronicle | ||||
| @ -119,7 +119,6 @@ const Revalidate = ( | ||||
|   res: NextApiResponse<ResponseMailProps> | ||||
| ): void => { | ||||
|   const body = req.body as RequestProps; | ||||
|   const { serverRuntimeConfig } = getConfig(); | ||||
| 
 | ||||
|   // Check for secret to confirm this is a valid request
 | ||||
|   if ( | ||||
| @ -135,7 +134,7 @@ const Revalidate = ( | ||||
|     case "post": { | ||||
|       paths.push(`/news`); | ||||
|       paths.push(`/news/${body.entry.slug}`); | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         paths.push(`/${locale}/news`); | ||||
|         paths.push(`/${locale}/news/${body.entry.slug}`); | ||||
|       }); | ||||
| @ -149,7 +148,7 @@ const Revalidate = ( | ||||
|       body.entry.subitem_of.forEach((parentItem) => { | ||||
|         paths.push(`/library/${parentItem.slug}`); | ||||
|       }); | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         paths.push(`/${locale}/library`); | ||||
|         paths.push(`/${locale}/library/${body.entry.slug}`); | ||||
|         paths.push(`/${locale}/library/${body.entry.slug}/scans`); | ||||
| @ -166,7 +165,7 @@ const Revalidate = ( | ||||
|       if (body.entry.folder?.slug) { | ||||
|         paths.push(`/contents/folder/${body.entry.folder.slug}`); | ||||
|       } | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         paths.push(`/${locale}/contents`); | ||||
|         paths.push(`/${locale}/contents/${body.entry.slug}`); | ||||
|         if (body.entry.folder?.slug) { | ||||
| @ -181,7 +180,7 @@ const Revalidate = ( | ||||
|           ); | ||||
|           paths.push(`/library/${parentSlug}`); | ||||
|           paths.push(`/library/${parentSlug}/scans`); | ||||
|           serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|           i18n.locales.forEach((locale: string) => { | ||||
|             paths.push(`/${locale}/library/${parentSlug}`); | ||||
|             paths.push(`/${locale}/library/${parentSlug}/scans`); | ||||
|           }); | ||||
| @ -193,7 +192,7 @@ const Revalidate = ( | ||||
|     case "chronology-era": | ||||
|     case "chronology-item": { | ||||
|       paths.push(`/wiki/chronology`); | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         paths.push(`/${locale}/wiki/chronology`); | ||||
|       }); | ||||
|       break; | ||||
| @ -207,7 +206,7 @@ const Revalidate = ( | ||||
|       if (body.entry.content) { | ||||
|         paths.push(`/contents/${body.entry.content.slug}`); | ||||
|       } | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         if (body.entry.library_item) { | ||||
|           paths.push(`/${locale}/library/${body.entry.library_item.slug}`); | ||||
|           paths.push( | ||||
| @ -235,7 +234,7 @@ const Revalidate = ( | ||||
|       body.entry.contents.forEach((content) => | ||||
|         paths.push(`/contents/${content.slug}`) | ||||
|       ); | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         if (body.entry.slug === "root") { | ||||
|           paths.push(`/${locale}/contents`); | ||||
|         } | ||||
| @ -258,7 +257,7 @@ const Revalidate = ( | ||||
|     case "wiki-page": { | ||||
|       paths.push(`/wiki`); | ||||
|       paths.push(`/wiki/${body.entry.slug}`); | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         paths.push(`/${locale}/wiki`); | ||||
|         paths.push(`/${locale}/wiki/${body.entry.slug}`); | ||||
|       }); | ||||
| @ -269,7 +268,7 @@ const Revalidate = ( | ||||
|     case "chronicle": { | ||||
|       paths.push(`/chronicles`); | ||||
|       paths.push(`/chronicles/${body.entry.slug}`); | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         paths.push(`/${locale}/chronicles`); | ||||
|         paths.push(`/${locale}/chronicles/${body.entry.slug}`); | ||||
|       }); | ||||
| @ -278,12 +277,12 @@ const Revalidate = ( | ||||
| 
 | ||||
|     case "chronicles-chapter": { | ||||
|       paths.push(`/chronicles`); | ||||
|       serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|       i18n.locales.forEach((locale: string) => { | ||||
|         paths.push(`/${locale}/chronicles`); | ||||
|       }); | ||||
|       body.entry.chronicles.forEach((chronicle) => { | ||||
|         paths.push(`/chronicles/${chronicle.slug}`); | ||||
|         serverRuntimeConfig.locales?.forEach((locale: string) => { | ||||
|         i18n.locales.forEach((locale: string) => { | ||||
|           paths.push(`/${locale}/chronicles/${chronicle.slug}`); | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| @ -21,7 +22,6 @@ import { Icon } from "components/Ico"; | ||||
| import { useMediaHoverable } from "hooks/useMediaQuery"; | ||||
| import { WithLabel } from "components/Inputs/WithLabel"; | ||||
| import { filterHasAttributes, isDefined } from "helpers/others"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| 
 | ||||
| /* | ||||
| @ -36,7 +36,7 @@ interface Props extends AppStaticProps, AppLayoutRequired { | ||||
| } | ||||
| 
 | ||||
| const Channel = ({ langui, channel, ...otherProps }: Props): JSX.Element => { | ||||
|   const { state: keepInfoVisible, toggleState: toggleKeepInfoVisible } = | ||||
|   const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = | ||||
|     useBoolean(true); | ||||
|   const hoverable = useMediaHoverable(); | ||||
| 
 | ||||
| @ -58,12 +58,9 @@ const Channel = ({ langui, channel, ...otherProps }: Props): JSX.Element => { | ||||
|         /> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|           <WithLabel label={langui.always_show_info}> | ||||
|             <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|           </WithLabel> | ||||
|         )} | ||||
|       </SubPanel> | ||||
|     ), | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { GetStaticProps } from "next"; | ||||
| import { useMemo, useState } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { SmartList } from "components/SmartList"; | ||||
| import { Icon } from "components/Ico"; | ||||
| @ -23,7 +24,6 @@ import { getReadySdk } from "graphql/sdk"; | ||||
| import { filterHasAttributes } from "helpers/others"; | ||||
| import { getVideoThumbnailURL } from "helpers/videos"; | ||||
| import { useMediaHoverable } from "hooks/useMediaQuery"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { compareDate } from "helpers/date"; | ||||
| 
 | ||||
| @ -48,7 +48,7 @@ interface Props extends AppStaticProps, AppLayoutRequired { | ||||
| const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => { | ||||
|   const hoverable = useMediaHoverable(); | ||||
| 
 | ||||
|   const { state: keepInfoVisible, toggleState: toggleKeepInfoVisible } = | ||||
|   const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = | ||||
|     useBoolean(true); | ||||
| 
 | ||||
|   const [searchName, setSearchName] = useState( | ||||
| @ -80,12 +80,9 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => { | ||||
|         /> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|           <WithLabel label={langui.always_show_info}> | ||||
|             <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|           </WithLabel> | ||||
|         )} | ||||
|       </SubPanel> | ||||
|     ), | ||||
|  | ||||
| @ -17,7 +17,6 @@ import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import { TranslatedChroniclesList } from "components/Translated"; | ||||
| import { Icon } from "components/Ico"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { | ||||
| @ -25,6 +24,7 @@ import { | ||||
|   staticSmartLanguage, | ||||
| } from "helpers/locales"; | ||||
| import { getDescription } from "helpers/description"; | ||||
| import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList"; | ||||
| 
 | ||||
| /* | ||||
|  *                                           ╭────────╮ | ||||
|  | ||||
| @ -9,8 +9,8 @@ import { getReadySdk } from "graphql/sdk"; | ||||
| import { GetChroniclesChaptersQuery } from "graphql/generated"; | ||||
| import { filterHasAttributes } from "helpers/others"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { TranslatedChroniclesList } from "components/Translated"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList"; | ||||
| 
 | ||||
| /* | ||||
|  *                                           ╭────────╮ | ||||
|  | ||||
| @ -5,7 +5,10 @@ import { Chip } from "components/Chip"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs"; | ||||
| import { Markdawn, TableOfContents } from "components/Markdown/Markdawn"; | ||||
| import { ReturnButtonType } from "components/PanelComponents/ReturnButton"; | ||||
| import { | ||||
|   ReturnButtonType, | ||||
|   TranslatedReturnButton, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import { ContentPanel } from "components/Panels/ContentPanel"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { PreviewCard } from "components/PreviewCard"; | ||||
| @ -30,16 +33,13 @@ import { ContentWithTranslations } from "helpers/types"; | ||||
| import { useMediaMobile } from "hooks/useMediaQuery"; | ||||
| import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { | ||||
|   TranslatedPreviewLine, | ||||
|   TranslatedReturnButton, | ||||
| } from "components/Translated"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { | ||||
|   getDefaultPreferredLanguages, | ||||
|   staticSmartLanguage, | ||||
| } from "helpers/locales"; | ||||
| import { getDescription } from "helpers/description"; | ||||
| import { TranslatedPreviewLine } from "components/PreviewLine"; | ||||
| 
 | ||||
| /* | ||||
|  *                                           ╭────────╮ | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { GetStaticProps } from "next"; | ||||
| import { useState, useMemo, useCallback } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { Select } from "components/Inputs/Select"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| @ -21,10 +22,9 @@ import { filterDefined, filterHasAttributes } from "helpers/others"; | ||||
| import { GetContentsQuery } from "graphql/generated"; | ||||
| import { SmartList } from "components/SmartList"; | ||||
| import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| import { TranslatedPreviewCard } from "components/Translated"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { TranslatedPreviewCard } from "components/PreviewCard"; | ||||
| 
 | ||||
| /* | ||||
|  *                                         ╭─────────────╮ | ||||
| @ -58,9 +58,9 @@ const Contents = ({ | ||||
|     DEFAULT_FILTERS_STATE.groupingMethod | ||||
|   ); | ||||
|   const { | ||||
|     state: keepInfoVisible, | ||||
|     toggleState: toggleKeepInfoVisible, | ||||
|     setState: setKeepInfoVisible, | ||||
|     value: keepInfoVisible, | ||||
|     toggle: toggleKeepInfoVisible, | ||||
|     setValue: setKeepInfoVisible, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
| 
 | ||||
|   const [searchName, setSearchName] = useState( | ||||
| @ -150,9 +150,7 @@ const Contents = ({ | ||||
|           onChange={setSearchName} | ||||
|         /> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.group_by} | ||||
|           input={ | ||||
|         <WithLabel label={langui.group_by}> | ||||
|           <Select | ||||
|             className="w-full" | ||||
|             options={[langui.category ?? "Category", langui.type ?? "Type"]} | ||||
| @ -160,16 +158,12 @@ const Contents = ({ | ||||
|             onChange={setGroupingMethod} | ||||
|             allowEmpty | ||||
|           /> | ||||
|           } | ||||
|         /> | ||||
|         </WithLabel> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|           <WithLabel label={langui.always_show_info}> | ||||
|             <Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|           </WithLabel> | ||||
|         )} | ||||
| 
 | ||||
|         <Button | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| import { useCallback, useMemo } from "react"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { | ||||
|   ContentPanel, | ||||
| @ -16,16 +16,14 @@ import { | ||||
| } from "helpers/locales"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { SmartList } from "components/SmartList"; | ||||
| import { | ||||
|   TranslatedButton, | ||||
|   TranslatedPreviewCard, | ||||
|   TranslatedPreviewFolder, | ||||
| } from "components/Translated"; | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { Button, TranslatedButton } from "components/Inputs/Button"; | ||||
| import { Link } from "components/Inputs/Link"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { TranslatedPreviewCard } from "components/PreviewCard"; | ||||
| 
 | ||||
| /* | ||||
|  *                                           ╭────────╮ | ||||
| @ -304,6 +302,30 @@ export const PreviewFolder = ({ | ||||
|   </Link> | ||||
| ); | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const TranslatedPreviewFolder = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<PreviewFolderProps, "title">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
|   return ( | ||||
|     <PreviewFolder | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| interface NoContentNorFolderMessageProps { | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Img } from "components/Img"; | ||||
| @ -53,7 +54,6 @@ import { WithLabel } from "components/Inputs/WithLabel"; | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { cJoin, cIf } from "helpers/className"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { getDescription } from "helpers/description"; | ||||
| 
 | ||||
| @ -85,7 +85,7 @@ const LibrarySlug = ({ | ||||
|   const hoverable = useMediaHoverable(); | ||||
|   const router = useRouter(); | ||||
|   const [openLightBox, LightBox] = useLightBox(); | ||||
|   const { state: keepInfoVisible, toggleState: toggleKeepInfoVisible } = | ||||
|   const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = | ||||
|     useBoolean(false); | ||||
| 
 | ||||
|   useScrollTopOnChange(AnchorIds.ContentPanel, [item]); | ||||
| @ -446,15 +446,12 @@ const LibrarySlug = ({ | ||||
|               </h2> | ||||
| 
 | ||||
|               {hoverable && ( | ||||
|                 <WithLabel | ||||
|                   label={langui.always_show_info} | ||||
|                   input={ | ||||
|                 <WithLabel label={langui.always_show_info}> | ||||
|                   <Switch | ||||
|                     onClick={toggleKeepInfoVisible} | ||||
|                     value={keepInfoVisible} | ||||
|                   /> | ||||
|                   } | ||||
|                 /> | ||||
|                 </WithLabel> | ||||
|               )} | ||||
| 
 | ||||
|               <div | ||||
| @ -725,7 +722,7 @@ const ContentLine = ({ | ||||
|   slug, | ||||
|   parentSlug, | ||||
| }: ContentLineProps): JSX.Element => { | ||||
|   const { state: isOpened, toggleState: toggleOpened } = useBoolean(false); | ||||
|   const { value: isOpened, toggle: toggleOpened } = useBoolean(false); | ||||
| 
 | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: content?.translations ?? [], | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { ScanSetCover } from "components/Library/ScanSetCover"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| @ -11,7 +10,10 @@ import { | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { GetLibraryItemScansQuery } from "graphql/generated"; | ||||
| import { | ||||
|   GetLibraryItemScansQuery, | ||||
|   UploadImageFragment, | ||||
| } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { | ||||
| @ -21,7 +23,9 @@ import { | ||||
| } from "helpers/formatters"; | ||||
| import { | ||||
|   filterHasAttributes, | ||||
|   getStatusDescription, | ||||
|   isDefined, | ||||
|   isDefinedAndNotEmpty, | ||||
|   sortRangedContent, | ||||
| } from "helpers/others"; | ||||
| import { useLightBox } from "hooks/useLightBox"; | ||||
| @ -29,8 +33,17 @@ import { isUntangibleGroupItem } from "helpers/libraryItem"; | ||||
| import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs"; | ||||
| import { PreviewCard } from "components/PreviewCard"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { TranslatedNavOption, TranslatedScanSet } from "components/Translated"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Img } from "components/Img"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { RecorderChip } from "components/RecorderChip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { isInteger } from "helpers/numbers"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| import { TranslatedNavOption } from "components/PanelComponents/NavOption"; | ||||
| 
 | ||||
| /* | ||||
|  *                                           ╭────────╮ | ||||
| @ -297,3 +310,434 @@ export const getStaticPaths: GetStaticPaths = async (context) => { | ||||
|     fallback: "blocking", | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
|  *                                    ╭──────────────────────╮ | ||||
|  * ───────────────────────────────────╯  PRIVATE COMPONENTS  ╰────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
|  * ───────────────────────────────────────╯  COMPONENT  ╰─────────────────────────────────────────── | ||||
|  */ | ||||
| 
 | ||||
| interface ScanSetProps { | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
|   scanSet: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         NonNullable< | ||||
|           NonNullable< | ||||
|             GetLibraryItemScansQuery["libraryItems"] | ||||
|           >["data"][number]["attributes"] | ||||
|         >["contents"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["scan_set"] | ||||
|   >; | ||||
|   id: string; | ||||
|   title: string; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   langui: AppStaticProps["langui"]; | ||||
|   content: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         NonNullable< | ||||
|           GetLibraryItemScansQuery["libraryItems"] | ||||
|         >["data"][number]["attributes"] | ||||
|       >["contents"] | ||||
|     >["data"][number]["attributes"] | ||||
|   >["content"]; | ||||
| } | ||||
| 
 | ||||
| const ScanSet = ({ | ||||
|   openLightBox, | ||||
|   scanSet, | ||||
|   id, | ||||
|   title, | ||||
|   languages, | ||||
|   langui, | ||||
|   content, | ||||
| }: ScanSetProps): JSX.Element => { | ||||
|   const [selectedScan, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: scanSet, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<ScanSetProps["scanSet"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|       transform: useCallback( | ||||
|         (item: NonNullable<ScanSetProps["scanSet"][number]>) => { | ||||
|           item.pages?.data.sort((a, b) => { | ||||
|             if ( | ||||
|               a.attributes && | ||||
|               b.attributes && | ||||
|               isDefinedAndNotEmpty(a.attributes.url) && | ||||
|               isDefinedAndNotEmpty(b.attributes.url) | ||||
|             ) { | ||||
|               let aName = getAssetFilename(a.attributes.url); | ||||
|               let bName = getAssetFilename(b.attributes.url); | ||||
| 
 | ||||
|               /* | ||||
|                * If the number is a succession of 0s, make the number | ||||
|                * incrementally smaller than 0 (i.e: 00 becomes -1) | ||||
|                */ | ||||
|               if (aName.replaceAll("0", "").length === 0) { | ||||
|                 aName = (1 - aName.length).toString(10); | ||||
|               } | ||||
|               if (bName.replaceAll("0", "").length === 0) { | ||||
|                 bName = (1 - bName.length).toString(10); | ||||
|               } | ||||
| 
 | ||||
|               if (isInteger(aName) && isInteger(bName)) { | ||||
|                 return parseInt(aName, 10) - parseInt(bName, 10); | ||||
|               } | ||||
|               return a.attributes.url.localeCompare(b.attributes.url); | ||||
|             } | ||||
|             return 0; | ||||
|           }); | ||||
|           return item; | ||||
|         }, | ||||
|         [] | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|   const pages = useMemo( | ||||
|     () => filterHasAttributes(selectedScan?.pages?.data, ["attributes"]), | ||||
|     [selectedScan] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {selectedScan && isDefined(pages) && ( | ||||
|         <div> | ||||
|           <div | ||||
|             className="flex flex-row flex-wrap place-items-center | ||||
|           gap-6 pt-10 text-base first-of-type:pt-0" | ||||
|           > | ||||
|             <h2 id={id} className="text-2xl"> | ||||
|               {title} | ||||
|             </h2> | ||||
| 
 | ||||
|             <Chip | ||||
|               text={ | ||||
|                 selectedScan.language?.data?.attributes?.code === | ||||
|                 selectedScan.source_language?.data?.attributes?.code | ||||
|                   ? langui.scan ?? "Scan" | ||||
|                   : langui.scanlation ?? "Scanlation" | ||||
|               } | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex flex-row flex-wrap place-items-center gap-4 pb-6"> | ||||
|             {content?.data?.attributes && | ||||
|               isDefinedAndNotEmpty(content.data.attributes.slug) && ( | ||||
|                 <Button | ||||
|                   href={`/contents/${content.data.attributes.slug}`} | ||||
|                   text={langui.open_content} | ||||
|                 /> | ||||
|               )} | ||||
| 
 | ||||
|             {languageSwitcherProps.locales.size > 1 && ( | ||||
|               <LanguageSwitcher {...languageSwitcherProps} /> | ||||
|             )} | ||||
| 
 | ||||
|             <div className="grid place-content-center place-items-center"> | ||||
|               <p className="font-headers font-bold">{langui.status}:</p> | ||||
|               <ToolTip | ||||
|                 content={getStatusDescription(selectedScan.status, langui)} | ||||
|                 maxWidth={"20rem"} | ||||
|               > | ||||
|                 <Chip text={selectedScan.status} /> | ||||
|               </ToolTip> | ||||
|             </div> | ||||
| 
 | ||||
|             {selectedScan.scanners && selectedScan.scanners.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{langui.scanners}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(selectedScan.scanners.data, [ | ||||
|                     "id", | ||||
|                     "attributes", | ||||
|                   ] as const).map((scanner) => ( | ||||
|                     <Fragment key={scanner.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={scanner.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{langui.cleaners}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(selectedScan.cleaners.data, [ | ||||
|                     "id", | ||||
|                     "attributes", | ||||
|                   ] as const).map((cleaner) => ( | ||||
|                     <Fragment key={cleaner.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={cleaner.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {selectedScan.typesetters && | ||||
|               selectedScan.typesetters.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold"> | ||||
|                     {langui.typesetters}: | ||||
|                   </p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(selectedScan.typesetters.data, [ | ||||
|                       "id", | ||||
|                       "attributes", | ||||
|                     ] as const).map((typesetter) => ( | ||||
|                       <Fragment key={typesetter.id}> | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={typesetter.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|             {isDefinedAndNotEmpty(selectedScan.notes) && ( | ||||
|               <ToolTip content={selectedScan.notes}> | ||||
|                 <Chip text={langui.notes ?? "Notes"} /> | ||||
|               </ToolTip> | ||||
|             )} | ||||
|           </div> | ||||
| 
 | ||||
|           <div | ||||
|             className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 | ||||
|              desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] mobile:grid-cols-2" | ||||
|           > | ||||
|             {pages.map((page, index) => ( | ||||
|               <div | ||||
|                 key={page.id} | ||||
|                 className="cursor-pointer transition-transform | ||||
|                 drop-shadow-shade-lg hover:scale-[1.02]" | ||||
|                 onClick={() => { | ||||
|                   const images = pages.map((image) => | ||||
|                     getAssetURL(image.attributes.url, ImageQuality.Large) | ||||
|                   ); | ||||
|                   openLightBox(images, index); | ||||
|                 }} | ||||
|               > | ||||
|                 <Img src={page.attributes} quality={ImageQuality.Small} /> | ||||
|               </div> | ||||
|             ))} | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| const TranslatedScanSet = ({ | ||||
|   translations, | ||||
|   fallback, | ||||
|   ...otherProps | ||||
| }: TranslatedProps<ScanSetProps, "title">): JSX.Element => { | ||||
|   const [selectedTranslation] = useSmartLanguage({ | ||||
|     items: translations, | ||||
|     languageExtractor: useCallback( | ||||
|       (item: { language: string }): string => item.language, | ||||
|       [] | ||||
|     ), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <ScanSet | ||||
|       title={selectedTranslation?.title ?? fallback.title} | ||||
|       {...otherProps} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| interface ScanSetCoverProps { | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
|   images: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         GetLibraryItemScansQuery["libraryItems"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["images"] | ||||
|   >; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| const ScanSetCover = ({ | ||||
|   openLightBox, | ||||
|   images, | ||||
|   languages, | ||||
|   langui, | ||||
| }: ScanSetCoverProps): JSX.Element => { | ||||
|   const [selectedScan, LanguageSwitcher, languageSwitcherProps] = | ||||
|     useSmartLanguage({ | ||||
|       items: images, | ||||
|       languages: languages, | ||||
|       languageExtractor: useCallback( | ||||
|         (item: NonNullable<ScanSetCoverProps["images"][number]>) => | ||||
|           item.language?.data?.attributes?.code, | ||||
|         [] | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|   const coverImages = useMemo(() => { | ||||
|     const memo: UploadImageFragment[] = []; | ||||
|     if (selectedScan?.obi_belt?.full?.data?.attributes) | ||||
|       memo.push(selectedScan.obi_belt.full.data.attributes); | ||||
|     if (selectedScan?.obi_belt?.inside_full?.data?.attributes) | ||||
|       memo.push(selectedScan.obi_belt.inside_full.data.attributes); | ||||
|     if (selectedScan?.dust_jacket?.full?.data?.attributes) | ||||
|       memo.push(selectedScan.dust_jacket.full.data.attributes); | ||||
|     if (selectedScan?.dust_jacket?.inside_full?.data?.attributes) | ||||
|       memo.push(selectedScan.dust_jacket.inside_full.data.attributes); | ||||
|     if (selectedScan?.cover?.full?.data?.attributes) | ||||
|       memo.push(selectedScan.cover.full.data.attributes); | ||||
|     if (selectedScan?.cover?.inside_full?.data?.attributes) | ||||
|       memo.push(selectedScan.cover.inside_full.data.attributes); | ||||
|     return memo; | ||||
|   }, [selectedScan]); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {coverImages.length > 0 && selectedScan && ( | ||||
|         <div> | ||||
|           <div | ||||
|             className="flex flex-row flex-wrap place-items-center | ||||
|           gap-6 pt-10 text-base first-of-type:pt-0" | ||||
|           > | ||||
|             <h2 id={"cover"} className="text-2xl"> | ||||
|               {langui.cover} | ||||
|             </h2> | ||||
| 
 | ||||
|             <Chip | ||||
|               text={ | ||||
|                 selectedScan.language?.data?.attributes?.code === | ||||
|                 selectedScan.source_language?.data?.attributes?.code | ||||
|                   ? langui.scan ?? "Scan" | ||||
|                   : langui.scanlation ?? "Scanlation" | ||||
|               } | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex flex-row flex-wrap place-items-center gap-4 pb-6"> | ||||
|             <LanguageSwitcher {...languageSwitcherProps} /> | ||||
| 
 | ||||
|             <div className="grid place-content-center place-items-center"> | ||||
|               <p className="font-headers font-bold">{langui.status}:</p> | ||||
|               <ToolTip | ||||
|                 content={getStatusDescription(selectedScan.status, langui)} | ||||
|                 maxWidth={"20rem"} | ||||
|               > | ||||
|                 <Chip text={selectedScan.status} /> | ||||
|               </ToolTip> | ||||
|             </div> | ||||
| 
 | ||||
|             {selectedScan.scanners && selectedScan.scanners.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{langui.scanners}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(selectedScan.scanners.data, [ | ||||
|                     "id", | ||||
|                     "attributes", | ||||
|                   ] as const).map((scanner) => ( | ||||
|                     <Fragment key={scanner.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={scanner.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{langui.cleaners}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(selectedScan.cleaners.data, [ | ||||
|                     "id", | ||||
|                     "attributes", | ||||
|                   ] as const).map((cleaner) => ( | ||||
|                     <Fragment key={cleaner.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={cleaner.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {selectedScan.typesetters && | ||||
|               selectedScan.typesetters.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold"> | ||||
|                     {langui.typesetters}: | ||||
|                   </p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(selectedScan.typesetters.data, [ | ||||
|                       "id", | ||||
|                       "attributes", | ||||
|                     ] as const).map((typesetter) => ( | ||||
|                       <Fragment key={typesetter.id}> | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={typesetter.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
|           </div> | ||||
| 
 | ||||
|           <div | ||||
|             className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 | ||||
|               last-of-type:border-0 desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] | ||||
|               mobile:grid-cols-2" | ||||
|           > | ||||
|             {coverImages.map((image, index) => ( | ||||
|               <div | ||||
|                 key={image.url} | ||||
|                 className="cursor-pointer transition-transform | ||||
|                   drop-shadow-shade-lg hover:scale-[1.02]" | ||||
|                 onClick={() => { | ||||
|                   const imgs = coverImages.map((img) => | ||||
|                     getAssetURL(img.url, ImageQuality.Large) | ||||
|                   ); | ||||
| 
 | ||||
|                   openLightBox(imgs, index); | ||||
|                 }} | ||||
|               > | ||||
|                 <Img src={image} quality={ImageQuality.Small} /> | ||||
|               </div> | ||||
|             ))} | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { GetStaticProps } from "next"; | ||||
| import { useState, useMemo, useCallback } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { Select } from "components/Inputs/Select"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| @ -28,7 +29,6 @@ import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { convertPrice } from "helpers/numbers"; | ||||
| import { SmartList } from "components/SmartList"; | ||||
| import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { compareDate } from "helpers/date"; | ||||
| 
 | ||||
| @ -71,27 +71,27 @@ const Library = ({ | ||||
|   ); | ||||
| 
 | ||||
|   const { | ||||
|     state: showSubitems, | ||||
|     toggleState: toggleShowSubitems, | ||||
|     setState: setShowSubitems, | ||||
|     value: showSubitems, | ||||
|     toggle: toggleShowSubitems, | ||||
|     setValue: setShowSubitems, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.showSubitems); | ||||
| 
 | ||||
|   const { | ||||
|     state: showPrimaryItems, | ||||
|     toggleState: toggleShowPrimaryItems, | ||||
|     setState: setShowPrimaryItems, | ||||
|     value: showPrimaryItems, | ||||
|     toggle: toggleShowPrimaryItems, | ||||
|     setValue: setShowPrimaryItems, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.showPrimaryItems); | ||||
| 
 | ||||
|   const { | ||||
|     state: showSecondaryItems, | ||||
|     toggleState: toggleShowSecondaryItems, | ||||
|     setState: setShowSecondaryItems, | ||||
|     value: showSecondaryItems, | ||||
|     toggle: toggleShowSecondaryItems, | ||||
|     setValue: setShowSecondaryItems, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.showSecondaryItems); | ||||
| 
 | ||||
|   const { | ||||
|     state: keepInfoVisible, | ||||
|     toggleState: toggleKeepInfoVisible, | ||||
|     setState: setKeepInfoVisible, | ||||
|     value: keepInfoVisible, | ||||
|     toggle: toggleKeepInfoVisible, | ||||
|     setValue: setKeepInfoVisible, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
| 
 | ||||
|   const [sortingMethod, setSortingMethod] = useState<number>( | ||||
| @ -268,9 +268,7 @@ const Library = ({ | ||||
|           onChange={setSearchName} | ||||
|         /> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.group_by} | ||||
|           input={ | ||||
|         <WithLabel label={langui.group_by}> | ||||
|           <Select | ||||
|             className="w-full" | ||||
|             options={[ | ||||
| @ -282,12 +280,9 @@ const Library = ({ | ||||
|             onChange={setGroupingMethod} | ||||
|             allowEmpty | ||||
|           /> | ||||
|           } | ||||
|         /> | ||||
|         </WithLabel> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.order_by} | ||||
|           input={ | ||||
|         <WithLabel label={langui.order_by}> | ||||
|           <Select | ||||
|             className="w-full" | ||||
|             options={[ | ||||
| @ -298,38 +293,27 @@ const Library = ({ | ||||
|             value={sortingMethod} | ||||
|             onChange={setSortingMethod} | ||||
|           /> | ||||
|           } | ||||
|         /> | ||||
|         </WithLabel> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.show_subitems} | ||||
|           input={<Switch value={showSubitems} onClick={toggleShowSubitems} />} | ||||
|         /> | ||||
|         <WithLabel label={langui.show_subitems}> | ||||
|           <Switch value={showSubitems} onClick={toggleShowSubitems} /> | ||||
|         </WithLabel> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.show_primary_items} | ||||
|           input={ | ||||
|         <WithLabel label={langui.show_primary_items}> | ||||
|           <Switch value={showPrimaryItems} onClick={toggleShowPrimaryItems} /> | ||||
|           } | ||||
|         /> | ||||
|         </WithLabel> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.show_secondary_items} | ||||
|           input={ | ||||
|         <WithLabel label={langui.show_secondary_items}> | ||||
|           <Switch | ||||
|             value={showSecondaryItems} | ||||
|             onClick={toggleShowSecondaryItems} | ||||
|           /> | ||||
|           } | ||||
|         /> | ||||
|         </WithLabel> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|           <WithLabel label={langui.always_show_info}> | ||||
|             <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|           </WithLabel> | ||||
|         )} | ||||
| 
 | ||||
|         <ButtonGroup | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { GetStaticProps } from "next"; | ||||
| import { useMemo, useState } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| @ -19,10 +20,9 @@ import { Button } from "components/Inputs/Button"; | ||||
| import { useMediaHoverable } from "hooks/useMediaQuery"; | ||||
| import { filterHasAttributes } from "helpers/others"; | ||||
| import { SmartList } from "components/SmartList"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| import { TranslatedPreviewCard } from "components/Translated"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { compareDate } from "helpers/date"; | ||||
| import { TranslatedPreviewCard } from "components/PreviewCard"; | ||||
| 
 | ||||
| /* | ||||
|  *                                         ╭─────────────╮ | ||||
| @ -49,9 +49,9 @@ const News = ({ langui, posts, ...otherProps }: Props): JSX.Element => { | ||||
|     DEFAULT_FILTERS_STATE.searchName | ||||
|   ); | ||||
|   const { | ||||
|     state: keepInfoVisible, | ||||
|     toggleState: toggleKeepInfoVisible, | ||||
|     setState: setKeepInfoVisible, | ||||
|     value: keepInfoVisible, | ||||
|     toggle: toggleKeepInfoVisible, | ||||
|     setValue: setKeepInfoVisible, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
| 
 | ||||
|   const subPanel = useMemo( | ||||
| @ -71,12 +71,9 @@ const News = ({ langui, posts, ...otherProps }: Props): JSX.Element => { | ||||
|         /> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|           <WithLabel label={langui.always_show_info}> | ||||
|             <Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|           </WithLabel> | ||||
|         )} | ||||
| 
 | ||||
|         <Button | ||||
|  | ||||
| @ -24,13 +24,14 @@ import { | ||||
|   isDefinedAndNotEmpty, | ||||
| } from "helpers/others"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { TranslatedNavOption, TranslatedProps } from "components/Translated"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Ico, Icon } from "components/Ico"; | ||||
| import { AnchorShare } from "components/AnchorShare"; | ||||
| import { datePickerToDate } from "helpers/date"; | ||||
| import { TranslatedProps } from "helpers/types/TranslatedProps"; | ||||
| import { TranslatedNavOption } from "components/PanelComponents/NavOption"; | ||||
| 
 | ||||
| /* | ||||
|  *                                           ╭────────╮ | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { GetStaticProps } from "next"; | ||||
| import { useCallback, useMemo, useState } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| @ -23,9 +24,8 @@ import { SmartList } from "components/SmartList"; | ||||
| import { Select } from "components/Inputs/Select"; | ||||
| import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { useBoolean } from "hooks/useBoolean"; | ||||
| import { TranslatedPreviewCard } from "components/Translated"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { TranslatedPreviewCard } from "components/PreviewCard"; | ||||
| 
 | ||||
| /* | ||||
|  *                                         ╭─────────────╮ | ||||
| @ -59,9 +59,9 @@ const Wiki = ({ langui, pages, ...otherProps }: Props): JSX.Element => { | ||||
|   ); | ||||
| 
 | ||||
|   const { | ||||
|     state: keepInfoVisible, | ||||
|     toggleState: toggleKeepInfoVisible, | ||||
|     setState: setKeepInfoVisible, | ||||
|     value: keepInfoVisible, | ||||
|     toggle: toggleKeepInfoVisible, | ||||
|     setValue: setKeepInfoVisible, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
| 
 | ||||
|   const subPanel = useMemo( | ||||
| @ -80,9 +80,7 @@ const Wiki = ({ langui, pages, ...otherProps }: Props): JSX.Element => { | ||||
|           onChange={setSearchName} | ||||
|         /> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.group_by} | ||||
|           input={ | ||||
|         <WithLabel label={langui.group_by}> | ||||
|           <Select | ||||
|             className="w-full" | ||||
|             options={[langui.category ?? "Category"]} | ||||
| @ -90,16 +88,12 @@ const Wiki = ({ langui, pages, ...otherProps }: Props): JSX.Element => { | ||||
|             onChange={setGroupingMethod} | ||||
|             allowEmpty | ||||
|           /> | ||||
|           } | ||||
|         /> | ||||
|         </WithLabel> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|           <WithLabel label={langui.always_show_info}> | ||||
|             <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|           </WithLabel> | ||||
|         )} | ||||
| 
 | ||||
|         <Button | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint