Continued using hooks
This commit is contained in:
		
							parent
							
								
									efcf01e8a0
								
							
						
					
					
						commit
						d0b91f9db6
					
				| @ -161,30 +161,30 @@ export function AppLayout(props: Props): JSX.Element { | ||||
|   }, [fontSize]); | ||||
| 
 | ||||
|   const defaultPreferredLanguages = useMemo(() => { | ||||
|     let list: string[] = []; | ||||
|     let memo: string[] = []; | ||||
|     if (isDefinedAndNotEmpty(router.locale) && router.locales) { | ||||
|       if (router.locale === "en") { | ||||
|         list = [router.locale]; | ||||
|         memo = [router.locale]; | ||||
|         router.locales.map((locale) => { | ||||
|           if (locale !== router.locale) list.push(locale); | ||||
|           if (locale !== router.locale) memo.push(locale); | ||||
|         }); | ||||
|       } else { | ||||
|         list = [router.locale, "en"]; | ||||
|         memo = [router.locale, "en"]; | ||||
|         router.locales.map((locale) => { | ||||
|           if (locale !== router.locale && locale !== "en") list.push(locale); | ||||
|           if (locale !== router.locale && locale !== "en") memo.push(locale); | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|     return list; | ||||
|     return memo; | ||||
|   }, [router.locale, router.locales]); | ||||
| 
 | ||||
|   const currencyOptions = useMemo(() => { | ||||
|     const list: string[] = []; | ||||
|     const memo: string[] = []; | ||||
|     filterHasAttributes(currencies).map((currentCurrency) => { | ||||
|       if (isDefinedAndNotEmpty(currentCurrency.attributes.code)) | ||||
|         list.push(currentCurrency.attributes.code); | ||||
|         memo.push(currentCurrency.attributes.code); | ||||
|     }); | ||||
|     return list; | ||||
|     return memo; | ||||
|   }, [currencies]); | ||||
| 
 | ||||
|   const [currencySelect, setCurrencySelect] = useState<number>(-1); | ||||
|  | ||||
| @ -157,12 +157,10 @@ export function ScanSet(props: Props): JSX.Element { | ||||
|                   {filterHasAttributes(selectedScan.cleaners.data).map( | ||||
|                     (cleaner) => ( | ||||
|                       <Fragment key={cleaner.id}> | ||||
|                         {cleaner.attributes && ( | ||||
|                           <RecorderChip | ||||
|                             langui={langui} | ||||
|                             recorder={cleaner.attributes} | ||||
|                           /> | ||||
|                         )} | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={cleaner.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ) | ||||
|                   )} | ||||
| @ -178,12 +176,10 @@ export function ScanSet(props: Props): JSX.Element { | ||||
|                     {filterHasAttributes(selectedScan.typesetters.data).map( | ||||
|                       (typesetter) => ( | ||||
|                         <Fragment key={typesetter.id}> | ||||
|                           {typesetter.attributes && ( | ||||
|                             <RecorderChip | ||||
|                               langui={langui} | ||||
|                               recorder={typesetter.attributes} | ||||
|                             /> | ||||
|                           )} | ||||
|                           <RecorderChip | ||||
|                             langui={langui} | ||||
|                             recorder={typesetter.attributes} | ||||
|                           /> | ||||
|                         </Fragment> | ||||
|                       ) | ||||
|                     )} | ||||
| @ -218,9 +214,7 @@ export function ScanSet(props: Props): JSX.Element { | ||||
|                   openLightBox(images, index); | ||||
|                 }} | ||||
|               > | ||||
|                 {page.attributes && ( | ||||
|                   <Img image={page.attributes} quality={ImageQuality.Small} /> | ||||
|                 )} | ||||
|                 <Img image={page.attributes} quality={ImageQuality.Small} /> | ||||
|               </div> | ||||
|             ))} | ||||
|           </div> | ||||
|  | ||||
| @ -11,7 +11,7 @@ import { getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { filterHasAttributes, getStatusDescription } from "helpers/others"; | ||||
| 
 | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { Fragment } from "react"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
| @ -35,19 +35,22 @@ export function ScanSetCover(props: Props): JSX.Element { | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|   }); | ||||
| 
 | ||||
|   const coverImages: UploadImageFragment[] = []; | ||||
|   if (selectedScan?.obi_belt?.full?.data?.attributes) | ||||
|     coverImages.push(selectedScan.obi_belt.full.data.attributes); | ||||
|   if (selectedScan?.obi_belt?.inside_full?.data?.attributes) | ||||
|     coverImages.push(selectedScan.obi_belt.inside_full.data.attributes); | ||||
|   if (selectedScan?.dust_jacket?.full?.data?.attributes) | ||||
|     coverImages.push(selectedScan.dust_jacket.full.data.attributes); | ||||
|   if (selectedScan?.dust_jacket?.inside_full?.data?.attributes) | ||||
|     coverImages.push(selectedScan.dust_jacket.inside_full.data.attributes); | ||||
|   if (selectedScan?.cover?.full?.data?.attributes) | ||||
|     coverImages.push(selectedScan.cover.full.data.attributes); | ||||
|   if (selectedScan?.cover?.inside_full?.data?.attributes) | ||||
|     coverImages.push(selectedScan.cover.inside_full.data.attributes); | ||||
|   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]); | ||||
| 
 | ||||
|   if (coverImages.length > 0) { | ||||
|     return ( | ||||
|  | ||||
| @ -67,105 +67,136 @@ export function PostPage(props: Props): JSX.Element { | ||||
|     [post.slug, post.thumbnail, selectedTranslation] | ||||
|   ); | ||||
| 
 | ||||
|   const subPanel = | ||||
|     returnHref || returnTitle || displayCredits || displayToc ? ( | ||||
|       <SubPanel> | ||||
|   const subPanel = useMemo( | ||||
|     () => | ||||
|       returnHref || returnTitle || displayCredits || displayToc ? ( | ||||
|         <SubPanel> | ||||
|           {returnHref && returnTitle && ( | ||||
|             <ReturnButton | ||||
|               href={returnHref} | ||||
|               title={returnTitle} | ||||
|               langui={langui} | ||||
|               displayOn={ReturnButtonType.Desktop} | ||||
|               horizontalLine | ||||
|             /> | ||||
|           )} | ||||
| 
 | ||||
|           {displayCredits && ( | ||||
|             <> | ||||
|               {selectedTranslation && ( | ||||
|                 <div className="grid grid-flow-col place-content-center place-items-center gap-2"> | ||||
|                   <p className="font-headers">{langui.status}:</p> | ||||
| 
 | ||||
|                   <ToolTip | ||||
|                     content={getStatusDescription( | ||||
|                       selectedTranslation.status, | ||||
|                       langui | ||||
|                     )} | ||||
|                     maxWidth={"20rem"} | ||||
|                   > | ||||
|                     <Chip>{selectedTranslation.status}</Chip> | ||||
|                   </ToolTip> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|               {post.authors && post.authors.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers">{"Authors"}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(post.authors.data).map((author) => ( | ||||
|                       <Fragment key={author.id}> | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={author.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|               <HorizontalLine /> | ||||
|             </> | ||||
|           )} | ||||
| 
 | ||||
|           {displayToc && <TOC text={body} title={title} />} | ||||
|         </SubPanel> | ||||
|       ) : undefined, | ||||
|     [ | ||||
|       body, | ||||
|       displayCredits, | ||||
|       displayToc, | ||||
|       langui, | ||||
|       post.authors, | ||||
|       returnHref, | ||||
|       returnTitle, | ||||
|       selectedTranslation, | ||||
|       title, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel> | ||||
|         {returnHref && returnTitle && ( | ||||
|           <ReturnButton | ||||
|             href={returnHref} | ||||
|             title={returnTitle} | ||||
|             langui={langui} | ||||
|             displayOn={ReturnButtonType.Desktop} | ||||
|             displayOn={ReturnButtonType.Mobile} | ||||
|             horizontalLine | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
|         {displayCredits && ( | ||||
|         {displayThumbnailHeader ? ( | ||||
|           <> | ||||
|             {selectedTranslation && ( | ||||
|               <div className="grid grid-flow-col place-content-center place-items-center gap-2"> | ||||
|                 <p className="font-headers">{langui.status}:</p> | ||||
| 
 | ||||
|                 <ToolTip | ||||
|                   content={getStatusDescription( | ||||
|                     selectedTranslation.status, | ||||
|                     langui | ||||
|                   )} | ||||
|                   maxWidth={"20rem"} | ||||
|                 > | ||||
|                   <Chip>{selectedTranslation.status}</Chip> | ||||
|                 </ToolTip> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {post.authors && post.authors.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers">{"Authors"}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(post.authors.data).map((author) => ( | ||||
|                     <Fragment key={author.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={author.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
|             <ThumbnailHeader | ||||
|               thumbnail={thumbnail} | ||||
|               title={title} | ||||
|               description={excerpt} | ||||
|               langui={langui} | ||||
|               categories={post.categories} | ||||
|               languageSwitcher={<LanguageSwitcher />} | ||||
|             /> | ||||
| 
 | ||||
|             <HorizontalLine /> | ||||
|           </> | ||||
|         ) : ( | ||||
|           <> | ||||
|             {displayLanguageSwitcher && ( | ||||
|               <div className="grid place-content-end place-items-start"> | ||||
|                 <LanguageSwitcher /> | ||||
|               </div> | ||||
|             )} | ||||
|             {displayTitle && ( | ||||
|               <h1 className="my-16 flex justify-center gap-3 text-center text-4xl"> | ||||
|                 {title} | ||||
|               </h1> | ||||
|             )} | ||||
|           </> | ||||
|         )} | ||||
| 
 | ||||
|         {displayToc && <TOC text={body} title={title} />} | ||||
|       </SubPanel> | ||||
|     ) : undefined; | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
|       {returnHref && returnTitle && ( | ||||
|         <ReturnButton | ||||
|           href={returnHref} | ||||
|           title={returnTitle} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Mobile} | ||||
|           horizontalLine | ||||
|         /> | ||||
|       )} | ||||
| 
 | ||||
|       {displayThumbnailHeader ? ( | ||||
|         <> | ||||
|           <ThumbnailHeader | ||||
|             thumbnail={thumbnail} | ||||
|             title={title} | ||||
|             description={excerpt} | ||||
|             langui={langui} | ||||
|             categories={post.categories} | ||||
|             languageSwitcher={<LanguageSwitcher />} | ||||
|           /> | ||||
| 
 | ||||
|           <HorizontalLine /> | ||||
|         </> | ||||
|       ) : ( | ||||
|         <> | ||||
|           {displayLanguageSwitcher && ( | ||||
|             <div className="grid place-content-end place-items-start"> | ||||
|               <LanguageSwitcher /> | ||||
|             </div> | ||||
|           )} | ||||
|           {displayTitle && ( | ||||
|             <h1 className="my-16 flex justify-center gap-3 text-center text-4xl"> | ||||
|               {title} | ||||
|             </h1> | ||||
|           )} | ||||
|         </> | ||||
|       )} | ||||
| 
 | ||||
|       {prependBody} | ||||
|       <Markdawn text={body} /> | ||||
|       {appendBody} | ||||
|     </ContentPanel> | ||||
|         {prependBody} | ||||
|         <Markdawn text={body} /> | ||||
|         {appendBody} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       LanguageSwitcher, | ||||
|       appendBody, | ||||
|       body, | ||||
|       displayLanguageSwitcher, | ||||
|       displayThumbnailHeader, | ||||
|       displayTitle, | ||||
|       excerpt, | ||||
|       langui, | ||||
|       post.categories, | ||||
|       prependBody, | ||||
|       returnHref, | ||||
|       returnTitle, | ||||
|       thumbnail, | ||||
|       title, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -85,7 +85,6 @@ export function ChronologyItemComponent(props: Props): JSX.Element { | ||||
|                           {translation.description && ( | ||||
|                             <p | ||||
|                               className={ | ||||
|                                 event.translations && | ||||
|                                 event.translations.length > 1 | ||||
|                                   ? `mt-2 whitespace-pre-line before:ml-[-1em] before:inline-block
 | ||||
|                                   before:w-4 before:text-dark before:content-['-']` | ||||
|  | ||||
| @ -208,7 +208,7 @@ export function sortBy( | ||||
|   orderByType: number, | ||||
|   items: Items, | ||||
|   currencies: AppStaticProps["currencies"] | ||||
| ): Items { | ||||
| ) { | ||||
|   switch (orderByType) { | ||||
|     case 0: | ||||
|       return items.sort((a, b) => { | ||||
|  | ||||
| @ -19,23 +19,18 @@ type SortContentProps = | ||||
|     >["contents"]; | ||||
| 
 | ||||
| export function sortContent(contents: SortContentProps) { | ||||
|   if (contents) { | ||||
|     const newContent = { ...contents }; | ||||
|     newContent?.data.sort((a, b) => { | ||||
|       if ( | ||||
|         a.attributes?.range[0]?.__typename === "ComponentRangePageRange" && | ||||
|         b.attributes?.range[0]?.__typename === "ComponentRangePageRange" | ||||
|       ) { | ||||
|         return ( | ||||
|           a.attributes.range[0].starting_page - | ||||
|           b.attributes.range[0].starting_page | ||||
|         ); | ||||
|       } | ||||
|       return 0; | ||||
|     }); | ||||
|     return newContent; | ||||
|   } | ||||
|   return contents; | ||||
|   contents?.data.sort((a, b) => { | ||||
|     if ( | ||||
|       a.attributes?.range[0]?.__typename === "ComponentRangePageRange" && | ||||
|       b.attributes?.range[0]?.__typename === "ComponentRangePageRange" | ||||
|     ) { | ||||
|       return ( | ||||
|         a.attributes.range[0].starting_page - | ||||
|         b.attributes.range[0].starting_page | ||||
|       ); | ||||
|     } | ||||
|     return 0; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function getStatusDescription( | ||||
|  | ||||
| @ -38,12 +38,12 @@ export function useSmartLanguage<T>( | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   const availableLocales = useMemo(() => { | ||||
|     const map = new Map<string, number>(); | ||||
|     const memo = new Map<string, number>(); | ||||
|     filterDefined(items).map((elem, index) => { | ||||
|       const result = languageExtractor(elem); | ||||
|       if (isDefined(result)) map.set(result, index); | ||||
|       if (isDefined(result)) memo.set(result, index); | ||||
|     }); | ||||
|     return map; | ||||
|     return memo; | ||||
|   }, [items, languageExtractor]); | ||||
| 
 | ||||
|   const [selectedTranslationIndex, setSelectedTranslationIndex] = useState< | ||||
|  | ||||
| @ -7,21 +7,25 @@ import { ContentPanel } from "components/Panels/ContentPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function FourOhFour(props: Props): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
|       <h1>404 - {langui.page_not_found}</h1> | ||||
|       <ReturnButton | ||||
|         href="/" | ||||
|         title="Home" | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Both} | ||||
|       /> | ||||
|     </ContentPanel> | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel> | ||||
|         <h1>404 - {langui.page_not_found}</h1> | ||||
|         <ReturnButton | ||||
|           href="/" | ||||
|           title="Home" | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Both} | ||||
|         /> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [langui] | ||||
|   ); | ||||
|   return <AppLayout navTitle="404" contentPanel={contentPanel} {...props} />; | ||||
| } | ||||
|  | ||||
| @ -7,21 +7,25 @@ import { ContentPanel } from "components/Panels/ContentPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function FiveHundred(props: Props): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
|       <h1>500 - Internal Server Error</h1> | ||||
|       <ReturnButton | ||||
|         href="/" | ||||
|         title="Home" | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Both} | ||||
|       /> | ||||
|     </ContentPanel> | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel> | ||||
|         <h1>500 - Internal Server Error</h1> | ||||
|         <ReturnButton | ||||
|           href="/" | ||||
|           title="Home" | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Both} | ||||
|         /> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [langui] | ||||
|   ); | ||||
|   return <AppLayout navTitle="500" contentPanel={contentPanel} {...props} />; | ||||
| } | ||||
|  | ||||
| @ -6,32 +6,35 @@ import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function AboutUs(props: Props): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
|         icon={Icon.Info} | ||||
|         title={langui.about_us} | ||||
|         description={langui.about_us_description} | ||||
|       /> | ||||
|       <NavOption | ||||
|         title={langui.accords_handbook} | ||||
|         url="/about-us/accords-handbook" | ||||
|         border | ||||
|       /> | ||||
|       <NavOption title={langui.legality} url="/about-us/legality" border /> | ||||
|       {/* <NavOption title={langui.members} url="/about-us/members" border /> */} | ||||
|       <NavOption | ||||
|         title={langui.sharing_policy} | ||||
|         url="/about-us/sharing-policy" | ||||
|         border | ||||
|       /> | ||||
|       <NavOption title={langui.contact_us} url="/about-us/contact" border /> | ||||
|     </SubPanel> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <PanelHeader | ||||
|           icon={Icon.Info} | ||||
|           title={langui.about_us} | ||||
|           description={langui.about_us_description} | ||||
|         /> | ||||
|         <NavOption | ||||
|           title={langui.accords_handbook} | ||||
|           url="/about-us/accords-handbook" | ||||
|           border | ||||
|         /> | ||||
|         <NavOption title={langui.legality} url="/about-us/legality" border /> | ||||
|         <NavOption | ||||
|           title={langui.sharing_policy} | ||||
|           url="/about-us/sharing-policy" | ||||
|           border | ||||
|         /> | ||||
|         <NavOption title={langui.contact_us} url="/about-us/contact" border /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [langui] | ||||
|   ); | ||||
|   return ( | ||||
|     <AppLayout navTitle={langui.about_us} subPanel={subPanel} {...props} /> | ||||
|  | ||||
| @ -6,20 +6,24 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { Icon } from "components/Ico"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function Archives(props: Props): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
|         icon={Icon.Inventory} | ||||
|         title={langui.archives} | ||||
|         description={langui.archives_description} | ||||
|       /> | ||||
|       <NavOption title={"Videos"} url="/archives/videos/" border /> | ||||
|     </SubPanel> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <PanelHeader | ||||
|           icon={Icon.Inventory} | ||||
|           title={langui.archives} | ||||
|           description={langui.archives_description} | ||||
|         /> | ||||
|         <NavOption title={"Videos"} url="/archives/videos/" border /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [langui] | ||||
|   ); | ||||
|   return ( | ||||
|     <AppLayout navTitle={langui.archives} subPanel={subPanel} {...props} /> | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { Fragment, useState } from "react"; | ||||
| import { Fragment, useState, useMemo } from "react"; | ||||
| import { Icon } from "components/Ico"; | ||||
| import { useMediaHoverable } from "hooks/useMediaQuery"; | ||||
| import { WithLabel } from "components/Inputs/WithLabel"; | ||||
| @ -37,67 +37,79 @@ export default function Channel(props: Props): JSX.Element { | ||||
|   const [keepInfoVisible, setKeepInfoVisible] = useState(true); | ||||
|   const hoverable = useMediaHoverable(); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href="/archives/videos/" | ||||
|         title={langui.videos} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Desktop} | ||||
|         className="mb-10" | ||||
|       /> | ||||
| 
 | ||||
|       <PanelHeader | ||||
|         icon={Icon.Movie} | ||||
|         title={langui.videos} | ||||
|         description={langui.archives_description} | ||||
|       /> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel | ||||
|           label={langui.always_show_info} | ||||
|           input={ | ||||
|             <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|           } | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <ReturnButton | ||||
|           href="/archives/videos/" | ||||
|           title={langui.videos} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Desktop} | ||||
|           className="mb-10" | ||||
|         /> | ||||
|       )} | ||||
|     </SubPanel> | ||||
| 
 | ||||
|         <PanelHeader | ||||
|           icon={Icon.Movie} | ||||
|           title={langui.videos} | ||||
|           description={langui.archives_description} | ||||
|         /> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|               <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|         )} | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [hoverable, keepInfoVisible, langui] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       <div className="mb-8"> | ||||
|         <h1 className="text-3xl">{channel?.title}</h1> | ||||
|         <p>{channel?.subscribers.toLocaleString()} subscribers</p> | ||||
|       </div> | ||||
|       <div | ||||
|         className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         <div className="mb-8"> | ||||
|           <h1 className="text-3xl">{channel?.title}</h1> | ||||
|           <p>{channel?.subscribers.toLocaleString()} subscribers</p> | ||||
|         </div> | ||||
|         <div | ||||
|           className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 | ||||
|         desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2" | ||||
|       > | ||||
|         {filterHasAttributes(channel?.videos?.data).map((video) => ( | ||||
|           <Fragment key={video.id}> | ||||
|             <PreviewCard | ||||
|               href={`/archives/videos/v/${video.attributes.uid}`} | ||||
|               title={video.attributes.title} | ||||
|               thumbnail={getVideoThumbnailURL(video.attributes.uid)} | ||||
|               thumbnailAspectRatio="16/9" | ||||
|               keepInfoVisible={keepInfoVisible} | ||||
|               metadata={{ | ||||
|                 release_date: video.attributes.published_date, | ||||
|                 views: video.attributes.views, | ||||
|                 author: channel?.title, | ||||
|                 position: "Top", | ||||
|               }} | ||||
|               hoverlay={{ | ||||
|                 __typename: "Video", | ||||
|                 duration: video.attributes.duration, | ||||
|               }} | ||||
|             /> | ||||
|           </Fragment> | ||||
|         ))} | ||||
|       </div> | ||||
|     </ContentPanel> | ||||
|         > | ||||
|           {filterHasAttributes(channel?.videos?.data).map((video) => ( | ||||
|             <Fragment key={video.id}> | ||||
|               <PreviewCard | ||||
|                 href={`/archives/videos/v/${video.attributes.uid}`} | ||||
|                 title={video.attributes.title} | ||||
|                 thumbnail={getVideoThumbnailURL(video.attributes.uid)} | ||||
|                 thumbnailAspectRatio="16/9" | ||||
|                 keepInfoVisible={keepInfoVisible} | ||||
|                 metadata={{ | ||||
|                   release_date: video.attributes.published_date, | ||||
|                   views: video.attributes.views, | ||||
|                   author: channel?.title, | ||||
|                   position: "Top", | ||||
|                 }} | ||||
|                 hoverlay={{ | ||||
|                   __typename: "Video", | ||||
|                   duration: video.attributes.duration, | ||||
|                 }} | ||||
|               /> | ||||
|             </Fragment> | ||||
|           ))} | ||||
|         </div> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       channel?.subscribers, | ||||
|       channel?.title, | ||||
|       channel?.videos?.data, | ||||
|       keepInfoVisible, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout | ||||
|       navTitle={langui.archives} | ||||
|  | ||||
| @ -22,110 +22,108 @@ import { filterHasAttributes } from "helpers/others"; | ||||
| import { getVideoThumbnailURL } from "helpers/videos"; | ||||
| import { useMediaHoverable } from "hooks/useMediaQuery"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { Fragment, useState } from "react"; | ||||
| import { Fragment, useMemo, useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   videos: NonNullable<GetVideosPreviewQuery["videos"]>["data"]; | ||||
| } | ||||
| 
 | ||||
| const ITEM_PER_PAGE = 50; | ||||
| 
 | ||||
| export default function Videos(props: Props): JSX.Element { | ||||
|   const { langui, videos } = props; | ||||
|   const hoverable = useMediaHoverable(); | ||||
| 
 | ||||
|   videos | ||||
|     .sort((a, b) => { | ||||
|       const dateA = a.attributes?.published_date | ||||
|         ? prettyDate(a.attributes.published_date) | ||||
|         : "9999"; | ||||
|       const dateB = b.attributes?.published_date | ||||
|         ? prettyDate(b.attributes.published_date) | ||||
|         : "9999"; | ||||
|       return dateA.localeCompare(dateB); | ||||
|     }) | ||||
|     .reverse(); | ||||
| 
 | ||||
|   const itemPerPage = 50; | ||||
|   const paginatedVideos: Props["videos"][] = []; | ||||
|   for (let index = 0; itemPerPage * index < videos.length; index += 1) { | ||||
|     paginatedVideos.push( | ||||
|       videos.slice(index * itemPerPage, (index + 1) * itemPerPage) | ||||
|     ); | ||||
|   } | ||||
|   const paginatedVideos = useMemo(() => { | ||||
|     const memo = []; | ||||
|     for (let index = 0; ITEM_PER_PAGE * index < videos.length; index += 1) { | ||||
|       memo.push( | ||||
|         videos.slice(index * ITEM_PER_PAGE, (index + 1) * ITEM_PER_PAGE) | ||||
|       ); | ||||
|     } | ||||
|     return memo; | ||||
|   }, [videos]); | ||||
| 
 | ||||
|   const [page, setPage] = useState(0); | ||||
|   const [keepInfoVisible, setKeepInfoVisible] = useState(true); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href="/archives/" | ||||
|         title={"Archives"} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Desktop} | ||||
|         className="mb-10" | ||||
|       /> | ||||
| 
 | ||||
|       <PanelHeader | ||||
|         icon={Icon.Movie} | ||||
|         title="Videos" | ||||
|         description={langui.archives_description} | ||||
|       /> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel | ||||
|           label={langui.always_show_info} | ||||
|           input={ | ||||
|             <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|           } | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <ReturnButton | ||||
|           href="/archives/" | ||||
|           title={"Archives"} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Desktop} | ||||
|           className="mb-10" | ||||
|         /> | ||||
|       )} | ||||
|     </SubPanel> | ||||
| 
 | ||||
|         <PanelHeader | ||||
|           icon={Icon.Movie} | ||||
|           title="Videos" | ||||
|           description={langui.archives_description} | ||||
|         /> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|               <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|         )} | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [hoverable, keepInfoVisible, langui] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       <PageSelector | ||||
|         maxPage={Math.floor(videos.length / itemPerPage)} | ||||
|         page={page} | ||||
|         setPage={setPage} | ||||
|         className="mb-12" | ||||
|       /> | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         <PageSelector | ||||
|           maxPage={Math.floor(videos.length / ITEM_PER_PAGE)} | ||||
|           page={page} | ||||
|           setPage={setPage} | ||||
|           className="mb-12" | ||||
|         /> | ||||
| 
 | ||||
|       <div | ||||
|         className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 | ||||
|         <div | ||||
|           className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 | ||||
|         desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2 | ||||
|         thin:grid-cols-1" | ||||
|       > | ||||
|         {filterHasAttributes(paginatedVideos[page]).map((video) => ( | ||||
|           <Fragment key={video.id}> | ||||
|             <PreviewCard | ||||
|               href={`/archives/videos/v/${video.attributes.uid}`} | ||||
|               title={video.attributes.title} | ||||
|               thumbnail={getVideoThumbnailURL(video.attributes.uid)} | ||||
|               thumbnailAspectRatio="16/9" | ||||
|               keepInfoVisible={keepInfoVisible} | ||||
|               metadata={{ | ||||
|                 release_date: video.attributes.published_date, | ||||
|                 views: video.attributes.views, | ||||
|                 author: video.attributes.channel?.data?.attributes?.title, | ||||
|                 position: "Top", | ||||
|               }} | ||||
|               hoverlay={{ | ||||
|                 __typename: "Video", | ||||
|                 duration: video.attributes.duration, | ||||
|               }} | ||||
|             /> | ||||
|           </Fragment> | ||||
|         ))} | ||||
|       </div> | ||||
|         > | ||||
|           {filterHasAttributes(paginatedVideos[page]).map((video) => ( | ||||
|             <Fragment key={video.id}> | ||||
|               <PreviewCard | ||||
|                 href={`/archives/videos/v/${video.attributes.uid}`} | ||||
|                 title={video.attributes.title} | ||||
|                 thumbnail={getVideoThumbnailURL(video.attributes.uid)} | ||||
|                 thumbnailAspectRatio="16/9" | ||||
|                 keepInfoVisible={keepInfoVisible} | ||||
|                 metadata={{ | ||||
|                   release_date: video.attributes.published_date, | ||||
|                   views: video.attributes.views, | ||||
|                   author: video.attributes.channel?.data?.attributes?.title, | ||||
|                   position: "Top", | ||||
|                 }} | ||||
|                 hoverlay={{ | ||||
|                   __typename: "Video", | ||||
|                   duration: video.attributes.duration, | ||||
|                 }} | ||||
|               /> | ||||
|             </Fragment> | ||||
|           ))} | ||||
|         </div> | ||||
| 
 | ||||
|       <PageSelector | ||||
|         maxPage={Math.floor(videos.length / itemPerPage)} | ||||
|         page={page} | ||||
|         setPage={setPage} | ||||
|         className="mt-12" | ||||
|       /> | ||||
|     </ContentPanel> | ||||
|         <PageSelector | ||||
|           maxPage={Math.floor(videos.length / ITEM_PER_PAGE)} | ||||
|           page={page} | ||||
|           setPage={setPage} | ||||
|           className="mt-12" | ||||
|         /> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [keepInfoVisible, page, paginatedVideos, videos.length] | ||||
|   ); | ||||
|   return ( | ||||
|     <AppLayout | ||||
| @ -143,6 +141,17 @@ export async function getStaticProps( | ||||
|   const sdk = getReadySdk(); | ||||
|   const videos = await sdk.getVideosPreview(); | ||||
|   if (!videos.videos) return { notFound: true }; | ||||
|   videos.videos.data | ||||
|     .sort((a, b) => { | ||||
|       const dateA = a.attributes?.published_date | ||||
|         ? prettyDate(a.attributes.published_date) | ||||
|         : "9999"; | ||||
|       const dateB = b.attributes?.published_date | ||||
|         ? prettyDate(b.attributes.published_date) | ||||
|         : "9999"; | ||||
|       return dateA.localeCompare(dateB); | ||||
|     }) | ||||
|     .reverse(); | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     videos: videos.videos.data, | ||||
|  | ||||
| @ -26,6 +26,7 @@ import { | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   video: NonNullable< | ||||
| @ -37,145 +38,164 @@ export default function Video(props: Props): JSX.Element { | ||||
|   const { langui, video } = props; | ||||
|   const isMobile = useMediaMobile(); | ||||
|   const appLayout = useAppLayout(); | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href="/archives/videos/" | ||||
|         title={langui.videos} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Desktop} | ||||
|         className="mb-10" | ||||
|       /> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <ReturnButton | ||||
|           href="/archives/videos/" | ||||
|           title={langui.videos} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Desktop} | ||||
|           className="mb-10" | ||||
|         /> | ||||
| 
 | ||||
|       <HorizontalLine /> | ||||
|         <HorizontalLine /> | ||||
| 
 | ||||
|       <NavOption | ||||
|         title={langui.video} | ||||
|         url="#video" | ||||
|         border | ||||
|         onClick={() => appLayout.setSubPanelOpen(false)} | ||||
|       /> | ||||
|         <NavOption | ||||
|           title={langui.video} | ||||
|           url="#video" | ||||
|           border | ||||
|           onClick={() => appLayout.setSubPanelOpen(false)} | ||||
|         /> | ||||
| 
 | ||||
|       <NavOption | ||||
|         title={langui.channel} | ||||
|         url="#channel" | ||||
|         border | ||||
|         onClick={() => appLayout.setSubPanelOpen(false)} | ||||
|       /> | ||||
|         <NavOption | ||||
|           title={langui.channel} | ||||
|           url="#channel" | ||||
|           border | ||||
|           onClick={() => appLayout.setSubPanelOpen(false)} | ||||
|         /> | ||||
| 
 | ||||
|       <NavOption | ||||
|         title={langui.description} | ||||
|         url="#description" | ||||
|         border | ||||
|         onClick={() => appLayout.setSubPanelOpen(false)} | ||||
|       /> | ||||
|     </SubPanel> | ||||
|         <NavOption | ||||
|           title={langui.description} | ||||
|           url="#description" | ||||
|           border | ||||
|           onClick={() => appLayout.setSubPanelOpen(false)} | ||||
|         /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [appLayout, langui] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       <ReturnButton | ||||
|         href="/library/" | ||||
|         title={langui.library} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Mobile} | ||||
|         className="mb-10" | ||||
|       /> | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         <ReturnButton | ||||
|           href="/library/" | ||||
|           title={langui.library} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Mobile} | ||||
|           className="mb-10" | ||||
|         /> | ||||
| 
 | ||||
|       <div className="grid place-items-center gap-12"> | ||||
|         <div | ||||
|           id="video" | ||||
|           className="w-full overflow-hidden rounded-xl shadow-lg shadow-shade" | ||||
|         > | ||||
|           {video.gone ? ( | ||||
|             <video | ||||
|               className="w-full" | ||||
|               src={getVideoFile(video.uid)} | ||||
|               controls | ||||
|             ></video> | ||||
|           ) : ( | ||||
|             <iframe | ||||
|               src={`https://www.youtube-nocookie.com/embed/${video.uid}`} | ||||
|               className="aspect-video w-full" | ||||
|               title="YouTube video player" | ||||
|               frameBorder="0" | ||||
|               allow="accelerometer; autoplay; clipboard-write; | ||||
|         <div className="grid place-items-center gap-12"> | ||||
|           <div | ||||
|             id="video" | ||||
|             className="w-full overflow-hidden rounded-xl shadow-lg shadow-shade" | ||||
|           > | ||||
|             {video.gone ? ( | ||||
|               <video | ||||
|                 className="w-full" | ||||
|                 src={getVideoFile(video.uid)} | ||||
|                 controls | ||||
|               ></video> | ||||
|             ) : ( | ||||
|               <iframe | ||||
|                 src={`https://www.youtube-nocookie.com/embed/${video.uid}`} | ||||
|                 className="aspect-video w-full" | ||||
|                 title="YouTube video player" | ||||
|                 frameBorder="0" | ||||
|                 allow="accelerometer; autoplay; clipboard-write; | ||||
|               encrypted-media; gyroscope; picture-in-picture" | ||||
|               allowFullScreen | ||||
|             ></iframe> | ||||
|           )} | ||||
|                 allowFullScreen | ||||
|               ></iframe> | ||||
|             )} | ||||
| 
 | ||||
|           <div className="mt-2 p-6"> | ||||
|             <h1 className="text-2xl">{video.title}</h1> | ||||
|             <div className="flex w-full flex-row flex-wrap gap-x-6"> | ||||
|               <p> | ||||
|                 <Ico | ||||
|                   icon={Icon.Event} | ||||
|                   className="mr-1 translate-y-[.15em] !text-base" | ||||
|                 /> | ||||
|                 {prettyDate(video.published_date)} | ||||
|               </p> | ||||
|               <p> | ||||
|                 <Ico | ||||
|                   icon={Icon.Visibility} | ||||
|                   className="mr-1 translate-y-[.15em] !text-base" | ||||
|                 /> | ||||
|                 {isMobile | ||||
|                   ? prettyShortenNumber(video.views) | ||||
|                   : video.views.toLocaleString()} | ||||
|               </p> | ||||
|               {video.channel?.data?.attributes && ( | ||||
|             <div className="mt-2 p-6"> | ||||
|               <h1 className="text-2xl">{video.title}</h1> | ||||
|               <div className="flex w-full flex-row flex-wrap gap-x-6"> | ||||
|                 <p> | ||||
|                   <Ico | ||||
|                     icon={Icon.ThumbUp} | ||||
|                     icon={Icon.Event} | ||||
|                     className="mr-1 translate-y-[.15em] !text-base" | ||||
|                   /> | ||||
|                   {prettyDate(video.published_date)} | ||||
|                 </p> | ||||
|                 <p> | ||||
|                   <Ico | ||||
|                     icon={Icon.Visibility} | ||||
|                     className="mr-1 translate-y-[.15em] !text-base" | ||||
|                   /> | ||||
|                   {isMobile | ||||
|                     ? prettyShortenNumber(video.likes) | ||||
|                     : video.likes.toLocaleString()} | ||||
|                 </p> | ||||
|               )} | ||||
|               <a | ||||
|                 href={`https://youtu.be/${video.uid}`} | ||||
|                 target="_blank" | ||||
|                 rel="noreferrer" | ||||
|               > | ||||
|                 <Button | ||||
|                   className="!py-0 !px-3" | ||||
|                   text={`${langui.view_on} ${video.source}`} | ||||
|                 /> | ||||
|               </a> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         {video.channel?.data?.attributes && ( | ||||
|           <InsetBox id="channel" className="grid place-items-center"> | ||||
|             <div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-4 text-center"> | ||||
|               <h2 className="text-2xl">{langui.channel}</h2> | ||||
|               <div> | ||||
|                 <Button | ||||
|                   href={`/archives/videos/c/${video.channel.data.attributes.uid}`} | ||||
|                   text={video.channel.data.attributes.title} | ||||
|                 /> | ||||
|                 <p> | ||||
|                   {`${video.channel.data.attributes.subscribers.toLocaleString()} | ||||
|                    ${langui.subscribers?.toLowerCase()}`}
 | ||||
|                     ? prettyShortenNumber(video.views) | ||||
|                     : video.views.toLocaleString()} | ||||
|                 </p> | ||||
|                 {video.channel?.data?.attributes && ( | ||||
|                   <p> | ||||
|                     <Ico | ||||
|                       icon={Icon.ThumbUp} | ||||
|                       className="mr-1 translate-y-[.15em] !text-base" | ||||
|                     /> | ||||
|                     {isMobile | ||||
|                       ? prettyShortenNumber(video.likes) | ||||
|                       : video.likes.toLocaleString()} | ||||
|                   </p> | ||||
|                 )} | ||||
|                 <a | ||||
|                   href={`https://youtu.be/${video.uid}`} | ||||
|                   target="_blank" | ||||
|                   rel="noreferrer" | ||||
|                 > | ||||
|                   <Button | ||||
|                     className="!py-0 !px-3" | ||||
|                     text={`${langui.view_on} ${video.source}`} | ||||
|                   /> | ||||
|                 </a> | ||||
|               </div> | ||||
|             </div> | ||||
|           </InsetBox> | ||||
|         )} | ||||
| 
 | ||||
|         <InsetBox id="description" className="grid place-items-center"> | ||||
|           <div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8"> | ||||
|             <h2 className="text-2xl">{langui.description}</h2> | ||||
|             <p className="whitespace-pre-line">{video.description}</p> | ||||
|           </div> | ||||
|         </InsetBox> | ||||
|       </div> | ||||
|     </ContentPanel> | ||||
| 
 | ||||
|           {video.channel?.data?.attributes && ( | ||||
|             <InsetBox id="channel" className="grid place-items-center"> | ||||
|               <div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-4 text-center"> | ||||
|                 <h2 className="text-2xl">{langui.channel}</h2> | ||||
|                 <div> | ||||
|                   <Button | ||||
|                     href={`/archives/videos/c/${video.channel.data.attributes.uid}`} | ||||
|                     text={video.channel.data.attributes.title} | ||||
|                   /> | ||||
|                   <p> | ||||
|                     {`${video.channel.data.attributes.subscribers.toLocaleString()} | ||||
|                    ${langui.subscribers?.toLowerCase()}`}
 | ||||
|                   </p> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </InsetBox> | ||||
|           )} | ||||
| 
 | ||||
|           <InsetBox id="description" className="grid place-items-center"> | ||||
|             <div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8"> | ||||
|               <h2 className="text-2xl">{langui.description}</h2> | ||||
|               <p className="whitespace-pre-line">{video.description}</p> | ||||
|             </div> | ||||
|           </InsetBox> | ||||
|         </div> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       isMobile, | ||||
|       langui, | ||||
|       video.channel?.data?.attributes, | ||||
|       video.description, | ||||
|       video.gone, | ||||
|       video.likes, | ||||
|       video.published_date, | ||||
|       video.source, | ||||
|       video.title, | ||||
|       video.uid, | ||||
|       video.views, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout | ||||
|       navTitle={langui.archives} | ||||
|  | ||||
| @ -5,20 +5,25 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { Icon } from "components/Ico"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function Chronicles(props: Props): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
|         icon={Icon.WatchLater} | ||||
|         title={langui.chronicles} | ||||
|         description={langui.chronicles_description} | ||||
|       /> | ||||
|     </SubPanel> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <PanelHeader | ||||
|           icon={Icon.WatchLater} | ||||
|           title={langui.chronicles} | ||||
|           description={langui.chronicles_description} | ||||
|         /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [langui] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout navTitle={langui.chronicles} subPanel={subPanel} {...props} /> | ||||
|   ); | ||||
|  | ||||
| @ -76,321 +76,349 @@ export default function Content(props: Props): JSX.Element { | ||||
|     [content.group, content.slug] | ||||
|   ); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href={`/contents`} | ||||
|         title={langui.contents} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Desktop} | ||||
|         horizontalLine | ||||
|       /> | ||||
| 
 | ||||
|       {selectedTranslation?.text_set?.source_language?.data?.attributes | ||||
|         ?.code !== undefined && ( | ||||
|         <div className="grid gap-5"> | ||||
|           <h2 className="text-xl"> | ||||
|             {selectedTranslation.text_set.source_language.data.attributes | ||||
|               .code === selectedTranslation.language?.data?.attributes?.code | ||||
|               ? langui.transcript_notice | ||||
|               : langui.translation_notice} | ||||
|           </h2> | ||||
| 
 | ||||
|           {selectedTranslation.text_set.source_language.data.attributes.code !== | ||||
|             selectedTranslation.language?.data?.attributes?.code && ( | ||||
|             <div className="grid place-items-center gap-2"> | ||||
|               <p className="font-headers">{langui.source_language}:</p> | ||||
|               <Chip> | ||||
|                 {prettyLanguage( | ||||
|                   selectedTranslation.text_set.source_language.data.attributes | ||||
|                     .code, | ||||
|                   languages | ||||
|                 )} | ||||
|               </Chip> | ||||
|             </div> | ||||
|           )} | ||||
| 
 | ||||
|           <div className="grid grid-flow-col place-content-center place-items-center gap-2"> | ||||
|             <p className="font-headers">{langui.status}:</p> | ||||
| 
 | ||||
|             <ToolTip | ||||
|               content={getStatusDescription( | ||||
|                 selectedTranslation.text_set.status, | ||||
|                 langui | ||||
|               )} | ||||
|               maxWidth={"20rem"} | ||||
|             > | ||||
|               <Chip>{selectedTranslation.text_set.status}</Chip> | ||||
|             </ToolTip> | ||||
|           </div> | ||||
| 
 | ||||
|           {selectedTranslation.text_set.transcribers && | ||||
|             selectedTranslation.text_set.transcribers.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers">{langui.transcribers}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes( | ||||
|                     selectedTranslation.text_set.transcribers.data | ||||
|                   ).map((recorder) => ( | ||||
|                     <Fragment key={recorder.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={recorder.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|           {selectedTranslation.text_set.translators && | ||||
|             selectedTranslation.text_set.translators.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers">{langui.translators}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes( | ||||
|                     selectedTranslation.text_set.translators.data | ||||
|                   ).map((recorder) => ( | ||||
|                     <Fragment key={recorder.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={recorder.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|           {selectedTranslation.text_set.proofreaders && | ||||
|             selectedTranslation.text_set.proofreaders.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers">{langui.proofreaders}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes( | ||||
|                     selectedTranslation.text_set.proofreaders.data | ||||
|                   ).map((recorder) => ( | ||||
|                     <Fragment key={recorder.id}> | ||||
|                       <RecorderChip | ||||
|                         langui={langui} | ||||
|                         recorder={recorder.attributes} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|           {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && ( | ||||
|             <div> | ||||
|               <p className="font-headers">{"Notes"}:</p> | ||||
|               <div className="grid place-content-center place-items-center gap-2"> | ||||
|                 <Markdawn text={selectedTranslation.text_set.notes} /> | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {content.ranged_contents?.data && | ||||
|         content.ranged_contents.data.length > 0 && ( | ||||
|           <> | ||||
|             <HorizontalLine /> | ||||
|             <div> | ||||
|               <p className="font-headers text-2xl">{langui.source}</p> | ||||
|               <div className="mt-6 grid place-items-center gap-6 text-left"> | ||||
|                 {content.ranged_contents.data.map((rangedContent) => { | ||||
|                   const libraryItem = | ||||
|                     rangedContent.attributes?.library_item?.data; | ||||
|                   if (libraryItem?.attributes && libraryItem.id) { | ||||
|                     return ( | ||||
|                       <div | ||||
|                         key={libraryItem.attributes.slug} | ||||
|                         className="mobile:w-[80%]" | ||||
|                       > | ||||
|                         <PreviewCard | ||||
|                           href={`/library/${libraryItem.attributes.slug}`} | ||||
|                           title={libraryItem.attributes.title} | ||||
|                           subtitle={libraryItem.attributes.subtitle} | ||||
|                           thumbnail={ | ||||
|                             libraryItem.attributes.thumbnail?.data?.attributes | ||||
|                           } | ||||
|                           thumbnailAspectRatio="21/29.7" | ||||
|                           topChips={ | ||||
|                             libraryItem.attributes.metadata && | ||||
|                             libraryItem.attributes.metadata.length > 0 && | ||||
|                             libraryItem.attributes.metadata[0] | ||||
|                               ? [ | ||||
|                                   prettyItemSubType( | ||||
|                                     libraryItem.attributes.metadata[0] | ||||
|                                   ), | ||||
|                                 ] | ||||
|                               : [] | ||||
|                           } | ||||
|                           bottomChips={libraryItem.attributes.categories?.data.map( | ||||
|                             (category) => category.attributes?.short ?? "" | ||||
|                           )} | ||||
|                           metadata={{ | ||||
|                             currencies: currencies, | ||||
|                             release_date: libraryItem.attributes.release_date, | ||||
|                             price: libraryItem.attributes.price, | ||||
|                             position: "Bottom", | ||||
|                           }} | ||||
|                           infoAppend={ | ||||
|                             <PreviewCardCTAs | ||||
|                               id={libraryItem.id} | ||||
|                               displayCTAs={ | ||||
|                                 !isUntangibleGroupItem( | ||||
|                                   libraryItem.attributes.metadata?.[0] | ||||
|                                 ) | ||||
|                               } | ||||
|                               langui={langui} | ||||
|                             /> | ||||
|                           } | ||||
|                         /> | ||||
|                       </div> | ||||
|                     ); | ||||
|                   } | ||||
|                   return <></>; | ||||
|                 })} | ||||
|               </div> | ||||
|             </div> | ||||
|           </> | ||||
|         )} | ||||
| 
 | ||||
|       {selectedTranslation?.text_set?.text && ( | ||||
|         <> | ||||
|           <HorizontalLine /> | ||||
|           <TOC | ||||
|             text={selectedTranslation.text_set.text} | ||||
|             title={prettyinlineTitle( | ||||
|               selectedTranslation.pre_title, | ||||
|               selectedTranslation.title, | ||||
|               selectedTranslation.subtitle | ||||
|             )} | ||||
|           /> | ||||
|         </> | ||||
|       )} | ||||
|     </SubPanel> | ||||
|   ); | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
|       <ReturnButton | ||||
|         href={`/contents`} | ||||
|         title={langui.contents} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Mobile} | ||||
|         className="mb-10" | ||||
|       /> | ||||
| 
 | ||||
|       <div className="grid place-items-center"> | ||||
|         <ThumbnailHeader | ||||
|           thumbnail={content.thumbnail?.data?.attributes} | ||||
|           pre_title={selectedTranslation?.pre_title} | ||||
|           title={selectedTranslation?.title} | ||||
|           subtitle={selectedTranslation?.subtitle} | ||||
|           description={selectedTranslation?.description} | ||||
|           type={content.type} | ||||
|           categories={content.categories} | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <ReturnButton | ||||
|           href={`/contents`} | ||||
|           title={langui.contents} | ||||
|           langui={langui} | ||||
|           languageSwitcher={<LanguageSwitcher />} | ||||
|           displayOn={ReturnButtonType.Desktop} | ||||
|           horizontalLine | ||||
|         /> | ||||
| 
 | ||||
|         {previousContent?.attributes && ( | ||||
|           <div className="mt-12 mb-8 w-full"> | ||||
|             <h2 className="mb-4 text-center text-2xl"> | ||||
|               {langui.previous_content} | ||||
|         {selectedTranslation?.text_set?.source_language?.data?.attributes | ||||
|           ?.code !== undefined && ( | ||||
|           <div className="grid gap-5"> | ||||
|             <h2 className="text-xl"> | ||||
|               {selectedTranslation.text_set.source_language.data.attributes | ||||
|                 .code === selectedTranslation.language?.data?.attributes?.code | ||||
|                 ? langui.transcript_notice | ||||
|                 : langui.translation_notice} | ||||
|             </h2> | ||||
|             <TranslatedPreviewLine | ||||
|               href={`/contents/${previousContent.attributes.slug}`} | ||||
|               translations={previousContent.attributes.translations?.map( | ||||
|                 (translation) => ({ | ||||
|                   pre_title: translation?.pre_title, | ||||
|                   title: translation?.title, | ||||
|                   subtitle: translation?.subtitle, | ||||
|                   language: translation?.language?.data?.attributes?.code, | ||||
|                 }) | ||||
| 
 | ||||
|             {selectedTranslation.text_set.source_language.data.attributes | ||||
|               .code !== | ||||
|               selectedTranslation.language?.data?.attributes?.code && ( | ||||
|               <div className="grid place-items-center gap-2"> | ||||
|                 <p className="font-headers">{langui.source_language}:</p> | ||||
|                 <Chip> | ||||
|                   {prettyLanguage( | ||||
|                     selectedTranslation.text_set.source_language.data.attributes | ||||
|                       .code, | ||||
|                     languages | ||||
|                   )} | ||||
|                 </Chip> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             <div className="grid grid-flow-col place-content-center place-items-center gap-2"> | ||||
|               <p className="font-headers">{langui.status}:</p> | ||||
| 
 | ||||
|               <ToolTip | ||||
|                 content={getStatusDescription( | ||||
|                   selectedTranslation.text_set.status, | ||||
|                   langui | ||||
|                 )} | ||||
|                 maxWidth={"20rem"} | ||||
|               > | ||||
|                 <Chip>{selectedTranslation.text_set.status}</Chip> | ||||
|               </ToolTip> | ||||
|             </div> | ||||
| 
 | ||||
|             {selectedTranslation.text_set.transcribers && | ||||
|               selectedTranslation.text_set.transcribers.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers">{langui.transcribers}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes( | ||||
|                       selectedTranslation.text_set.transcribers.data | ||||
|                     ).map((recorder) => ( | ||||
|                       <Fragment key={recorder.id}> | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={recorder.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
|               slug={previousContent.attributes.slug} | ||||
|               languages={languages} | ||||
|               thumbnail={previousContent.attributes.thumbnail?.data?.attributes} | ||||
|               thumbnailAspectRatio="3/2" | ||||
|               topChips={ | ||||
|                 isMobile | ||||
|                   ? undefined | ||||
|                   : previousContent.attributes.type?.data?.attributes | ||||
|                   ? [ | ||||
|                       previousContent.attributes.type.data.attributes | ||||
|                         .titles?.[0] | ||||
|                         ? previousContent.attributes.type.data.attributes | ||||
|                             .titles[0]?.title | ||||
|                         : prettySlug( | ||||
|                             previousContent.attributes.type.data.attributes.slug | ||||
|                           ), | ||||
|                     ] | ||||
|                   : undefined | ||||
|               } | ||||
|               bottomChips={ | ||||
|                 isMobile | ||||
|                   ? undefined | ||||
|                   : previousContent.attributes.categories?.data.map( | ||||
|                       (category) => category.attributes?.short ?? "" | ||||
|                     ) | ||||
|               } | ||||
|             /> | ||||
| 
 | ||||
|             {selectedTranslation.text_set.translators && | ||||
|               selectedTranslation.text_set.translators.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers">{langui.translators}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes( | ||||
|                       selectedTranslation.text_set.translators.data | ||||
|                     ).map((recorder) => ( | ||||
|                       <Fragment key={recorder.id}> | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={recorder.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|             {selectedTranslation.text_set.proofreaders && | ||||
|               selectedTranslation.text_set.proofreaders.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers">{langui.proofreaders}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes( | ||||
|                       selectedTranslation.text_set.proofreaders.data | ||||
|                     ).map((recorder) => ( | ||||
|                       <Fragment key={recorder.id}> | ||||
|                         <RecorderChip | ||||
|                           langui={langui} | ||||
|                           recorder={recorder.attributes} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|             {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && ( | ||||
|               <div> | ||||
|                 <p className="font-headers">{"Notes"}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   <Markdawn text={selectedTranslation.text_set.notes} /> | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
|           </div> | ||||
|         )} | ||||
| 
 | ||||
|         <HorizontalLine /> | ||||
|         {content.ranged_contents?.data && | ||||
|           content.ranged_contents.data.length > 0 && ( | ||||
|             <> | ||||
|               <HorizontalLine /> | ||||
|               <div> | ||||
|                 <p className="font-headers text-2xl">{langui.source}</p> | ||||
|                 <div className="mt-6 grid place-items-center gap-6 text-left"> | ||||
|                   {content.ranged_contents.data.map((rangedContent) => { | ||||
|                     const libraryItem = | ||||
|                       rangedContent.attributes?.library_item?.data; | ||||
|                     if (libraryItem?.attributes && libraryItem.id) { | ||||
|                       return ( | ||||
|                         <div | ||||
|                           key={libraryItem.attributes.slug} | ||||
|                           className="mobile:w-[80%]" | ||||
|                         > | ||||
|                           <PreviewCard | ||||
|                             href={`/library/${libraryItem.attributes.slug}`} | ||||
|                             title={libraryItem.attributes.title} | ||||
|                             subtitle={libraryItem.attributes.subtitle} | ||||
|                             thumbnail={ | ||||
|                               libraryItem.attributes.thumbnail?.data?.attributes | ||||
|                             } | ||||
|                             thumbnailAspectRatio="21/29.7" | ||||
|                             topChips={ | ||||
|                               libraryItem.attributes.metadata && | ||||
|                               libraryItem.attributes.metadata.length > 0 && | ||||
|                               libraryItem.attributes.metadata[0] | ||||
|                                 ? [ | ||||
|                                     prettyItemSubType( | ||||
|                                       libraryItem.attributes.metadata[0] | ||||
|                                     ), | ||||
|                                   ] | ||||
|                                 : [] | ||||
|                             } | ||||
|                             bottomChips={libraryItem.attributes.categories?.data.map( | ||||
|                               (category) => category.attributes?.short ?? "" | ||||
|                             )} | ||||
|                             metadata={{ | ||||
|                               currencies: currencies, | ||||
|                               release_date: libraryItem.attributes.release_date, | ||||
|                               price: libraryItem.attributes.price, | ||||
|                               position: "Bottom", | ||||
|                             }} | ||||
|                             infoAppend={ | ||||
|                               <PreviewCardCTAs | ||||
|                                 id={libraryItem.id} | ||||
|                                 displayCTAs={ | ||||
|                                   !isUntangibleGroupItem( | ||||
|                                     libraryItem.attributes.metadata?.[0] | ||||
|                                   ) | ||||
|                                 } | ||||
|                                 langui={langui} | ||||
|                               /> | ||||
|                             } | ||||
|                           /> | ||||
|                         </div> | ||||
|                       ); | ||||
|                     } | ||||
|                     return <></>; | ||||
|                   })} | ||||
|                 </div> | ||||
|               </div> | ||||
|             </> | ||||
|           )} | ||||
| 
 | ||||
|         <Markdawn text={selectedTranslation?.text_set?.text ?? ""} /> | ||||
| 
 | ||||
|         {nextContent?.attributes && ( | ||||
|         {selectedTranslation?.text_set?.text && ( | ||||
|           <> | ||||
|             <HorizontalLine /> | ||||
|             <h2 className="mb-4 text-center text-2xl"> | ||||
|               {langui.followup_content} | ||||
|             </h2> | ||||
|             <TranslatedPreviewLine | ||||
|               href={`/contents/${nextContent.attributes.slug}`} | ||||
|               translations={nextContent.attributes.translations?.map( | ||||
|                 (translation) => ({ | ||||
|                   pre_title: translation?.pre_title, | ||||
|                   title: translation?.title, | ||||
|                   subtitle: translation?.subtitle, | ||||
|                   language: translation?.language?.data?.attributes?.code, | ||||
|                 }) | ||||
|             <TOC | ||||
|               text={selectedTranslation.text_set.text} | ||||
|               title={prettyinlineTitle( | ||||
|                 selectedTranslation.pre_title, | ||||
|                 selectedTranslation.title, | ||||
|                 selectedTranslation.subtitle | ||||
|               )} | ||||
|               slug={nextContent.attributes.slug} | ||||
|               languages={languages} | ||||
|               thumbnail={nextContent.attributes.thumbnail?.data?.attributes} | ||||
|               thumbnailAspectRatio="3/2" | ||||
|               topChips={ | ||||
|                 isMobile | ||||
|                   ? undefined | ||||
|                   : nextContent.attributes.type?.data?.attributes | ||||
|                   ? [ | ||||
|                       nextContent.attributes.type.data.attributes.titles?.[0] | ||||
|                         ? nextContent.attributes.type.data.attributes.titles[0] | ||||
|                             ?.title | ||||
|                         : prettySlug( | ||||
|                             nextContent.attributes.type.data.attributes.slug | ||||
|                           ), | ||||
|                     ] | ||||
|                   : undefined | ||||
|               } | ||||
|               bottomChips={ | ||||
|                 isMobile | ||||
|                   ? undefined | ||||
|                   : nextContent.attributes.categories?.data.map( | ||||
|                       (category) => category.attributes?.short ?? "" | ||||
|                     ) | ||||
|               } | ||||
|             /> | ||||
|           </> | ||||
|         )} | ||||
|       </div> | ||||
|     </ContentPanel> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [ | ||||
|       content.ranged_contents?.data, | ||||
|       currencies, | ||||
|       languages, | ||||
|       langui, | ||||
|       selectedTranslation, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel> | ||||
|         <ReturnButton | ||||
|           href={`/contents`} | ||||
|           title={langui.contents} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Mobile} | ||||
|           className="mb-10" | ||||
|         /> | ||||
| 
 | ||||
|         <div className="grid place-items-center"> | ||||
|           <ThumbnailHeader | ||||
|             thumbnail={content.thumbnail?.data?.attributes} | ||||
|             pre_title={selectedTranslation?.pre_title} | ||||
|             title={selectedTranslation?.title} | ||||
|             subtitle={selectedTranslation?.subtitle} | ||||
|             description={selectedTranslation?.description} | ||||
|             type={content.type} | ||||
|             categories={content.categories} | ||||
|             langui={langui} | ||||
|             languageSwitcher={<LanguageSwitcher />} | ||||
|           /> | ||||
| 
 | ||||
|           {previousContent?.attributes && ( | ||||
|             <div className="mt-12 mb-8 w-full"> | ||||
|               <h2 className="mb-4 text-center text-2xl"> | ||||
|                 {langui.previous_content} | ||||
|               </h2> | ||||
|               <TranslatedPreviewLine | ||||
|                 href={`/contents/${previousContent.attributes.slug}`} | ||||
|                 translations={previousContent.attributes.translations?.map( | ||||
|                   (translation) => ({ | ||||
|                     pre_title: translation?.pre_title, | ||||
|                     title: translation?.title, | ||||
|                     subtitle: translation?.subtitle, | ||||
|                     language: translation?.language?.data?.attributes?.code, | ||||
|                   }) | ||||
|                 )} | ||||
|                 slug={previousContent.attributes.slug} | ||||
|                 languages={languages} | ||||
|                 thumbnail={ | ||||
|                   previousContent.attributes.thumbnail?.data?.attributes | ||||
|                 } | ||||
|                 thumbnailAspectRatio="3/2" | ||||
|                 topChips={ | ||||
|                   isMobile | ||||
|                     ? undefined | ||||
|                     : previousContent.attributes.type?.data?.attributes | ||||
|                     ? [ | ||||
|                         previousContent.attributes.type.data.attributes | ||||
|                           .titles?.[0] | ||||
|                           ? previousContent.attributes.type.data.attributes | ||||
|                               .titles[0]?.title | ||||
|                           : prettySlug( | ||||
|                               previousContent.attributes.type.data.attributes | ||||
|                                 .slug | ||||
|                             ), | ||||
|                       ] | ||||
|                     : undefined | ||||
|                 } | ||||
|                 bottomChips={ | ||||
|                   isMobile | ||||
|                     ? undefined | ||||
|                     : previousContent.attributes.categories?.data.map( | ||||
|                         (category) => category.attributes?.short ?? "" | ||||
|                       ) | ||||
|                 } | ||||
|               /> | ||||
|             </div> | ||||
|           )} | ||||
| 
 | ||||
|           <HorizontalLine /> | ||||
| 
 | ||||
|           <Markdawn text={selectedTranslation?.text_set?.text ?? ""} /> | ||||
| 
 | ||||
|           {nextContent?.attributes && ( | ||||
|             <> | ||||
|               <HorizontalLine /> | ||||
|               <h2 className="mb-4 text-center text-2xl"> | ||||
|                 {langui.followup_content} | ||||
|               </h2> | ||||
|               <TranslatedPreviewLine | ||||
|                 href={`/contents/${nextContent.attributes.slug}`} | ||||
|                 translations={nextContent.attributes.translations?.map( | ||||
|                   (translation) => ({ | ||||
|                     pre_title: translation?.pre_title, | ||||
|                     title: translation?.title, | ||||
|                     subtitle: translation?.subtitle, | ||||
|                     language: translation?.language?.data?.attributes?.code, | ||||
|                   }) | ||||
|                 )} | ||||
|                 slug={nextContent.attributes.slug} | ||||
|                 languages={languages} | ||||
|                 thumbnail={nextContent.attributes.thumbnail?.data?.attributes} | ||||
|                 thumbnailAspectRatio="3/2" | ||||
|                 topChips={ | ||||
|                   isMobile | ||||
|                     ? undefined | ||||
|                     : nextContent.attributes.type?.data?.attributes | ||||
|                     ? [ | ||||
|                         nextContent.attributes.type.data.attributes.titles?.[0] | ||||
|                           ? nextContent.attributes.type.data.attributes | ||||
|                               .titles[0]?.title | ||||
|                           : prettySlug( | ||||
|                               nextContent.attributes.type.data.attributes.slug | ||||
|                             ), | ||||
|                       ] | ||||
|                     : undefined | ||||
|                 } | ||||
|                 bottomChips={ | ||||
|                   isMobile | ||||
|                     ? undefined | ||||
|                     : nextContent.attributes.categories?.data.map( | ||||
|                         (category) => category.attributes?.short ?? "" | ||||
|                       ) | ||||
|                 } | ||||
|               /> | ||||
|             </> | ||||
|           )} | ||||
|         </div> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       LanguageSwitcher, | ||||
|       content.categories, | ||||
|       content.thumbnail?.data?.attributes, | ||||
|       content.type, | ||||
|       isMobile, | ||||
|       languages, | ||||
|       langui, | ||||
|       nextContent?.attributes, | ||||
|       previousContent?.attributes, | ||||
|       selectedTranslation, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -71,166 +71,188 @@ export default function Contents(props: Props): JSX.Element { | ||||
|     [combineRelatedContent, searchName.length] | ||||
|   ); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
|         icon={Icon.Workspaces} | ||||
|         title={langui.contents} | ||||
|         description={langui.contents_description} | ||||
|       /> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <PanelHeader | ||||
|           icon={Icon.Workspaces} | ||||
|           title={langui.contents} | ||||
|           description={langui.contents_description} | ||||
|         /> | ||||
| 
 | ||||
|       <TextInput | ||||
|         className="mb-6 w-full" | ||||
|         placeholder={langui.search_title ?? undefined} | ||||
|         state={searchName} | ||||
|         setState={setSearchName} | ||||
|       /> | ||||
|         <TextInput | ||||
|           className="mb-6 w-full" | ||||
|           placeholder={langui.search_title ?? undefined} | ||||
|           state={searchName} | ||||
|           setState={setSearchName} | ||||
|         /> | ||||
| 
 | ||||
|       <WithLabel | ||||
|         label={langui.group_by} | ||||
|         input={ | ||||
|           <Select | ||||
|             className="w-full" | ||||
|             options={[langui.category ?? "", langui.type ?? ""]} | ||||
|             state={groupingMethod} | ||||
|             setState={setGroupingMethod} | ||||
|             allowEmpty | ||||
|           /> | ||||
|         } | ||||
|       /> | ||||
| 
 | ||||
|       <WithLabel | ||||
|         label={langui.combine_related_contents} | ||||
|         disabled={searchName.length > 1} | ||||
|         input={ | ||||
|           <Switch | ||||
|             setState={setCombineRelatedContent} | ||||
|             state={effectiveCombineRelatedContent} | ||||
|           /> | ||||
|         } | ||||
|       /> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel | ||||
|           label={langui.always_show_info} | ||||
|           label={langui.group_by} | ||||
|           input={ | ||||
|             <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|             <Select | ||||
|               className="w-full" | ||||
|               options={[langui.category ?? "", langui.type ?? ""]} | ||||
|               state={groupingMethod} | ||||
|               setState={setGroupingMethod} | ||||
|               allowEmpty | ||||
|             /> | ||||
|           } | ||||
|         /> | ||||
|       )} | ||||
| 
 | ||||
|       <Button | ||||
|         className="mt-8" | ||||
|         text={langui.reset_all_filters} | ||||
|         icon={Icon.Replay} | ||||
|         onClick={() => { | ||||
|           setSearchName(defaultFiltersState.searchName); | ||||
|           setGroupingMethod(defaultFiltersState.groupingMethod); | ||||
|           setKeepInfoVisible(defaultFiltersState.keepInfoVisible); | ||||
|           setCombineRelatedContent(defaultFiltersState.combineRelatedContent); | ||||
|         }} | ||||
|       /> | ||||
|     </SubPanel> | ||||
|   ); | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       {/* TODO: Add to langui */} | ||||
|       {groups.size === 0 && ( | ||||
|         <ContentPlaceholder | ||||
|           message={ | ||||
|             "No results. You can try changing or resetting the search parameters." | ||||
|         <WithLabel | ||||
|           label={langui.combine_related_contents} | ||||
|           disabled={searchName.length > 1} | ||||
|           input={ | ||||
|             <Switch | ||||
|               setState={setCombineRelatedContent} | ||||
|               state={effectiveCombineRelatedContent} | ||||
|             /> | ||||
|           } | ||||
|           icon={Icon.ChevronLeft} | ||||
|         /> | ||||
|       )} | ||||
|       {iterateMap( | ||||
|         groups, | ||||
|         (name, items, index) => | ||||
|           items.length > 0 && ( | ||||
|             <Fragment key={index}> | ||||
|               {name && ( | ||||
|                 <h2 | ||||
|                   className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|               <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
|         <Button | ||||
|           className="mt-8" | ||||
|           text={langui.reset_all_filters} | ||||
|           icon={Icon.Replay} | ||||
|           onClick={() => { | ||||
|             setSearchName(defaultFiltersState.searchName); | ||||
|             setGroupingMethod(defaultFiltersState.groupingMethod); | ||||
|             setKeepInfoVisible(defaultFiltersState.keepInfoVisible); | ||||
|             setCombineRelatedContent(defaultFiltersState.combineRelatedContent); | ||||
|           }} | ||||
|         /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [ | ||||
|       effectiveCombineRelatedContent, | ||||
|       groupingMethod, | ||||
|       hoverable, | ||||
|       keepInfoVisible, | ||||
|       langui, | ||||
|       searchName, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         {/* TODO: Add to langui */} | ||||
|         {groups.size === 0 && ( | ||||
|           <ContentPlaceholder | ||||
|             message={ | ||||
|               "No results. You can try changing or resetting the search parameters." | ||||
|             } | ||||
|             icon={Icon.ChevronLeft} | ||||
|           /> | ||||
|         )} | ||||
|         {iterateMap( | ||||
|           groups, | ||||
|           (name, items, index) => | ||||
|             items.length > 0 && ( | ||||
|               <Fragment key={index}> | ||||
|                 {name && ( | ||||
|                   <h2 | ||||
|                     className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl | ||||
|                 first-of-type:pt-0" | ||||
|                 > | ||||
|                   {name} | ||||
|                   <Chip>{`${items.reduce((currentSum, item) => { | ||||
|                     if (effectiveCombineRelatedContent) { | ||||
|                       if ( | ||||
|                         item.attributes?.group?.data?.attributes?.combine === | ||||
|                         true | ||||
|                       ) { | ||||
|                         return ( | ||||
|                           currentSum + | ||||
|                           (item.attributes.group.data.attributes.contents?.data | ||||
|                             .length ?? 1) | ||||
|                         ); | ||||
|                       } | ||||
|                     } | ||||
|                     return currentSum + 1; | ||||
|                   }, 0)} ${ | ||||
|                     items.length <= 1 | ||||
|                       ? langui.result?.toLowerCase() ?? "" | ||||
|                       : langui.results?.toLowerCase() ?? "" | ||||
|                   }`}</Chip>
 | ||||
|                 </h2> | ||||
|               )} | ||||
| 
 | ||||
|               <div | ||||
|                 className="grid grid-cols-2 items-end gap-8 | ||||
|                 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4" | ||||
|               > | ||||
|                 {filterHasAttributes(items).map((item) => ( | ||||
|                   <Fragment key={item.id}> | ||||
|                     <TranslatedPreviewCard | ||||
|                       href={`/contents/${item.attributes.slug}`} | ||||
|                       translations={item.attributes.translations?.map( | ||||
|                         (translation) => ({ | ||||
|                           pre_title: translation?.pre_title, | ||||
|                           title: translation?.title, | ||||
|                           subtitle: translation?.subtitle, | ||||
|                           language: | ||||
|                             translation?.language?.data?.attributes?.code, | ||||
|                         }) | ||||
|                       )} | ||||
|                       slug={item.attributes.slug} | ||||
|                       languages={languages} | ||||
|                       thumbnail={item.attributes.thumbnail?.data?.attributes} | ||||
|                       thumbnailAspectRatio="3/2" | ||||
|                       thumbnailForceAspectRatio | ||||
|                       stackNumber={ | ||||
|                         effectiveCombineRelatedContent && | ||||
|                         item.attributes.group?.data?.attributes?.combine === | ||||
|                   > | ||||
|                     {name} | ||||
|                     <Chip>{`${items.reduce((currentSum, item) => { | ||||
|                       if (effectiveCombineRelatedContent) { | ||||
|                         if ( | ||||
|                           item.attributes?.group?.data?.attributes?.combine === | ||||
|                           true | ||||
|                           ? item.attributes.group.data.attributes.contents?.data | ||||
|                               .length | ||||
|                           : 0 | ||||
|                         ) { | ||||
|                           return ( | ||||
|                             currentSum + | ||||
|                             (item.attributes.group.data.attributes.contents | ||||
|                               ?.data.length ?? 1) | ||||
|                           ); | ||||
|                         } | ||||
|                       } | ||||
|                       topChips={ | ||||
|                         item.attributes.type?.data?.attributes | ||||
|                           ? [ | ||||
|                               item.attributes.type.data.attributes.titles?.[0] | ||||
|                                 ? item.attributes.type.data.attributes.titles[0] | ||||
|                                     ?.title | ||||
|                                 : prettySlug( | ||||
|                                     item.attributes.type.data.attributes.slug | ||||
|                                   ), | ||||
|                             ] | ||||
|                           : undefined | ||||
|                       } | ||||
|                       bottomChips={item.attributes.categories?.data.map( | ||||
|                         (category) => category.attributes?.short ?? "" | ||||
|                       )} | ||||
|                       keepInfoVisible={keepInfoVisible} | ||||
|                     /> | ||||
|                   </Fragment> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </Fragment> | ||||
|           ) | ||||
|       )} | ||||
|     </ContentPanel> | ||||
|                       return currentSum + 1; | ||||
|                     }, 0)} ${ | ||||
|                       items.length <= 1 | ||||
|                         ? langui.result?.toLowerCase() ?? "" | ||||
|                         : langui.results?.toLowerCase() ?? "" | ||||
|                     }`}</Chip>
 | ||||
|                   </h2> | ||||
|                 )} | ||||
| 
 | ||||
|                 <div | ||||
|                   className="grid grid-cols-2 items-end gap-8 | ||||
|                 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4" | ||||
|                 > | ||||
|                   {filterHasAttributes(items).map((item) => ( | ||||
|                     <Fragment key={item.id}> | ||||
|                       <TranslatedPreviewCard | ||||
|                         href={`/contents/${item.attributes.slug}`} | ||||
|                         translations={item.attributes.translations?.map( | ||||
|                           (translation) => ({ | ||||
|                             pre_title: translation?.pre_title, | ||||
|                             title: translation?.title, | ||||
|                             subtitle: translation?.subtitle, | ||||
|                             language: | ||||
|                               translation?.language?.data?.attributes?.code, | ||||
|                           }) | ||||
|                         )} | ||||
|                         slug={item.attributes.slug} | ||||
|                         languages={languages} | ||||
|                         thumbnail={item.attributes.thumbnail?.data?.attributes} | ||||
|                         thumbnailAspectRatio="3/2" | ||||
|                         thumbnailForceAspectRatio | ||||
|                         stackNumber={ | ||||
|                           effectiveCombineRelatedContent && | ||||
|                           item.attributes.group?.data?.attributes?.combine === | ||||
|                             true | ||||
|                             ? item.attributes.group.data.attributes.contents | ||||
|                                 ?.data.length | ||||
|                             : 0 | ||||
|                         } | ||||
|                         topChips={ | ||||
|                           item.attributes.type?.data?.attributes | ||||
|                             ? [ | ||||
|                                 item.attributes.type.data.attributes.titles?.[0] | ||||
|                                   ? item.attributes.type.data.attributes | ||||
|                                       .titles[0]?.title | ||||
|                                   : prettySlug( | ||||
|                                       item.attributes.type.data.attributes.slug | ||||
|                                     ), | ||||
|                               ] | ||||
|                             : undefined | ||||
|                         } | ||||
|                         bottomChips={item.attributes.categories?.data.map( | ||||
|                           (category) => category.attributes?.short ?? "" | ||||
|                         )} | ||||
|                         keepInfoVisible={keepInfoVisible} | ||||
|                       /> | ||||
|                     </Fragment> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </Fragment> | ||||
|             ) | ||||
|         )} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       effectiveCombineRelatedContent, | ||||
|       groups, | ||||
|       keepInfoVisible, | ||||
|       languages, | ||||
|       langui.result, | ||||
|       langui.results, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout | ||||
|       navTitle={langui.contents} | ||||
|  | ||||
| @ -12,6 +12,7 @@ import { getReadySdk } from "graphql/sdk"; | ||||
| import { filterDefined, filterHasAttributes } from "helpers/others"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   contents: DevGetContentsQuery; | ||||
| @ -21,61 +22,65 @@ export default function CheckupContents(props: Props): JSX.Element { | ||||
|   const { contents } = props; | ||||
|   const testReport = testingContent(contents); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       {<h2 className="text-2xl">{testReport.title}</h2>} | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         {<h2 className="text-2xl">{testReport.title}</h2>} | ||||
| 
 | ||||
|       <div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2"> | ||||
|         <p></p> | ||||
|         <p></p> | ||||
|         <p className="font-headers">Ref</p> | ||||
|         <p className="font-headers">Name</p> | ||||
|         <p className="font-headers">Type</p> | ||||
|         <p className="font-headers">Severity</p> | ||||
|         <p className="font-headers">Description</p> | ||||
|       </div> | ||||
| 
 | ||||
|       {testReport.lines.map((line, index) => ( | ||||
|         <div | ||||
|           key={index} | ||||
|           className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center | ||||
|           justify-items-start gap-2" | ||||
|         > | ||||
|           <Button | ||||
|             href={line.frontendUrl} | ||||
|             target="_blank" | ||||
|             className="w-4 text-xs" | ||||
|             text="F" | ||||
|           /> | ||||
|           <Button | ||||
|             href={line.backendUrl} | ||||
|             target="_blank" | ||||
|             className="w-4 text-xs" | ||||
|             text="B" | ||||
|           /> | ||||
|           <p>{line.subitems.join(" -> ")}</p> | ||||
|           <p>{line.name}</p> | ||||
|           <Chip>{line.type}</Chip> | ||||
|           <Chip | ||||
|             className={ | ||||
|               line.severity === "Very High" | ||||
|                 ? "bg-[#f00] font-bold !opacity-100" | ||||
|                 : line.severity === "High" | ||||
|                 ? "bg-[#ff6600] font-bold !opacity-100" | ||||
|                 : line.severity === "Medium" | ||||
|                 ? "bg-[#fff344] !opacity-100" | ||||
|                 : "" | ||||
|             } | ||||
|           > | ||||
|             {line.severity} | ||||
|           </Chip> | ||||
|           <ToolTip content={line.recommandation} placement="left"> | ||||
|             <p>{line.description}</p> | ||||
|           </ToolTip> | ||||
|         <div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2"> | ||||
|           <p></p> | ||||
|           <p></p> | ||||
|           <p className="font-headers">Ref</p> | ||||
|           <p className="font-headers">Name</p> | ||||
|           <p className="font-headers">Type</p> | ||||
|           <p className="font-headers">Severity</p> | ||||
|           <p className="font-headers">Description</p> | ||||
|         </div> | ||||
|       ))} | ||||
|     </ContentPanel> | ||||
| 
 | ||||
|         {testReport.lines.map((line, index) => ( | ||||
|           <div | ||||
|             key={index} | ||||
|             className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center | ||||
|           justify-items-start gap-2" | ||||
|           > | ||||
|             <Button | ||||
|               href={line.frontendUrl} | ||||
|               target="_blank" | ||||
|               className="w-4 text-xs" | ||||
|               text="F" | ||||
|             /> | ||||
|             <Button | ||||
|               href={line.backendUrl} | ||||
|               target="_blank" | ||||
|               className="w-4 text-xs" | ||||
|               text="B" | ||||
|             /> | ||||
|             <p>{line.subitems.join(" -> ")}</p> | ||||
|             <p>{line.name}</p> | ||||
|             <Chip>{line.type}</Chip> | ||||
|             <Chip | ||||
|               className={ | ||||
|                 line.severity === "Very High" | ||||
|                   ? "bg-[#f00] font-bold !opacity-100" | ||||
|                   : line.severity === "High" | ||||
|                   ? "bg-[#ff6600] font-bold !opacity-100" | ||||
|                   : line.severity === "Medium" | ||||
|                   ? "bg-[#fff344] !opacity-100" | ||||
|                   : "" | ||||
|               } | ||||
|             > | ||||
|               {line.severity} | ||||
|             </Chip> | ||||
|             <ToolTip content={line.recommandation} placement="left"> | ||||
|               <p>{line.description}</p> | ||||
|             </ToolTip> | ||||
|           </div> | ||||
|         ))} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [testReport.lines, testReport.title] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} /> | ||||
|   ); | ||||
|  | ||||
| @ -14,6 +14,7 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   libraryItems: DevGetLibraryItemsQuery; | ||||
| @ -23,61 +24,65 @@ export default function CheckupLibraryItems(props: Props): JSX.Element { | ||||
|   const { libraryItems } = props; | ||||
|   const testReport = testingLibraryItem(libraryItems); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       {<h2 className="text-2xl">{testReport.title}</h2>} | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         {<h2 className="text-2xl">{testReport.title}</h2>} | ||||
| 
 | ||||
|       <div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2"> | ||||
|         <p></p> | ||||
|         <p></p> | ||||
|         <p className="font-headers">Ref</p> | ||||
|         <p className="font-headers">Name</p> | ||||
|         <p className="font-headers">Type</p> | ||||
|         <p className="font-headers">Severity</p> | ||||
|         <p className="font-headers">Description</p> | ||||
|       </div> | ||||
| 
 | ||||
|       {testReport.lines.map((line, index) => ( | ||||
|         <div | ||||
|           key={index} | ||||
|           className="mb-2 grid | ||||
|           grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2" | ||||
|         > | ||||
|           <Button | ||||
|             href={line.frontendUrl} | ||||
|             target="_blank" | ||||
|             className="w-4 text-xs" | ||||
|             text="F" | ||||
|           /> | ||||
|           <Button | ||||
|             href={line.backendUrl} | ||||
|             target="_blank" | ||||
|             className="w-4 text-xs" | ||||
|             text="B" | ||||
|           /> | ||||
|           <p>{line.subitems.join(" -> ")}</p> | ||||
|           <p>{line.name}</p> | ||||
|           <Chip>{line.type}</Chip> | ||||
|           <Chip | ||||
|             className={ | ||||
|               line.severity === "Very High" | ||||
|                 ? "bg-[#f00] font-bold !opacity-100" | ||||
|                 : line.severity === "High" | ||||
|                 ? "bg-[#ff6600] font-bold !opacity-100" | ||||
|                 : line.severity === "Medium" | ||||
|                 ? "bg-[#fff344] !opacity-100" | ||||
|                 : "" | ||||
|             } | ||||
|           > | ||||
|             {line.severity} | ||||
|           </Chip> | ||||
|           <ToolTip content={line.recommandation} placement="left"> | ||||
|             <p>{line.description}</p> | ||||
|           </ToolTip> | ||||
|         <div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2"> | ||||
|           <p></p> | ||||
|           <p></p> | ||||
|           <p className="font-headers">Ref</p> | ||||
|           <p className="font-headers">Name</p> | ||||
|           <p className="font-headers">Type</p> | ||||
|           <p className="font-headers">Severity</p> | ||||
|           <p className="font-headers">Description</p> | ||||
|         </div> | ||||
|       ))} | ||||
|     </ContentPanel> | ||||
| 
 | ||||
|         {testReport.lines.map((line, index) => ( | ||||
|           <div | ||||
|             key={index} | ||||
|             className="mb-2 grid | ||||
|           grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2" | ||||
|           > | ||||
|             <Button | ||||
|               href={line.frontendUrl} | ||||
|               target="_blank" | ||||
|               className="w-4 text-xs" | ||||
|               text="F" | ||||
|             /> | ||||
|             <Button | ||||
|               href={line.backendUrl} | ||||
|               target="_blank" | ||||
|               className="w-4 text-xs" | ||||
|               text="B" | ||||
|             /> | ||||
|             <p>{line.subitems.join(" -> ")}</p> | ||||
|             <p>{line.name}</p> | ||||
|             <Chip>{line.type}</Chip> | ||||
|             <Chip | ||||
|               className={ | ||||
|                 line.severity === "Very High" | ||||
|                   ? "bg-[#f00] font-bold !opacity-100" | ||||
|                   : line.severity === "High" | ||||
|                   ? "bg-[#ff6600] font-bold !opacity-100" | ||||
|                   : line.severity === "Medium" | ||||
|                   ? "bg-[#fff344] !opacity-100" | ||||
|                   : "" | ||||
|               } | ||||
|             > | ||||
|               {line.severity} | ||||
|             </Chip> | ||||
|             <ToolTip content={line.recommandation} placement="left"> | ||||
|               <p>{line.description}</p> | ||||
|             </ToolTip> | ||||
|           </div> | ||||
|         ))} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [testReport.lines, testReport.title] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout navTitle={"Checkup"} contentPanel={contentPanel} {...props} /> | ||||
|   ); | ||||
|  | ||||
| @ -10,7 +10,7 @@ import { ToolTip } from "components/ToolTip"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { useCallback, useState } from "react"; | ||||
| import { useCallback, useMemo, useState } from "react"; | ||||
| import TurndownService from "turndown"; | ||||
| import { Icon } from "components/Ico"; | ||||
| import { TOC } from "components/Markdown/TOC"; | ||||
| @ -25,408 +25,445 @@ export default function Editor(props: Props): JSX.Element { | ||||
|   const [markdown, setMarkdown] = useState(""); | ||||
|   const [converterOpened, setConverterOpened] = useState(false); | ||||
| 
 | ||||
|   function wrap( | ||||
|     wrapper: string, | ||||
|     properties?: Record<string, string>, | ||||
|     addInnerNewLines?: boolean | ||||
|   ) { | ||||
|     transformationWrapper((value, selectionStart, selectionEnd) => { | ||||
|       let prepend = wrapper; | ||||
|       let append = wrapper; | ||||
|   const transformationWrapper = useCallback( | ||||
|     ( | ||||
|       transformation: ( | ||||
|         value: string, | ||||
|         selectionStart: number, | ||||
|         selectedEnd: number | ||||
|       ) => { prependLength: number; transformedValue: string } | ||||
|     ) => { | ||||
|       const textarea = | ||||
|         document.querySelector<HTMLTextAreaElement>("#editorTextArea"); | ||||
|       if (textarea) { | ||||
|         const { value, selectionStart, selectionEnd } = textarea; | ||||
| 
 | ||||
|       if (properties) { | ||||
|         prepend = `<${wrapper}${Object.entries(properties).map( | ||||
|           ([propertyName, propertyValue]) => | ||||
|             ` ${propertyName}="${propertyValue}"` | ||||
|         )}>`;
 | ||||
|         append = `</${wrapper}>`; | ||||
|         const { prependLength, transformedValue } = transformation( | ||||
|           value, | ||||
|           selectionStart, | ||||
|           selectionEnd | ||||
|         ); | ||||
| 
 | ||||
|         textarea.value = transformedValue; | ||||
|         handleInput(textarea.value); | ||||
| 
 | ||||
|         textarea.focus(); | ||||
|         textarea.selectionStart = selectionStart + prependLength; | ||||
|         textarea.selectionEnd = selectionEnd + prependLength; | ||||
|       } | ||||
|     }, | ||||
|     [handleInput] | ||||
|   ); | ||||
| 
 | ||||
|       if (addInnerNewLines === true) { | ||||
|         prepend = `${prepend}\n`; | ||||
|         append = `\n${append}`; | ||||
|   const wrap = useCallback( | ||||
|     ( | ||||
|       wrapper: string, | ||||
|       properties?: Record<string, string>, | ||||
|       addInnerNewLines?: boolean | ||||
|     ) => { | ||||
|       transformationWrapper((value, selectionStart, selectionEnd) => { | ||||
|         let prepend = wrapper; | ||||
|         let append = wrapper; | ||||
| 
 | ||||
|         if (properties) { | ||||
|           prepend = `<${wrapper}${Object.entries(properties).map( | ||||
|             ([propertyName, propertyValue]) => | ||||
|               ` ${propertyName}="${propertyValue}"` | ||||
|           )}>`;
 | ||||
|           append = `</${wrapper}>`; | ||||
|         } | ||||
| 
 | ||||
|         if (addInnerNewLines === true) { | ||||
|           prepend = `${prepend}\n`; | ||||
|           append = `\n${append}`; | ||||
|         } | ||||
| 
 | ||||
|         let newValue = ""; | ||||
|         newValue += value.slice(0, selectionStart); | ||||
|         newValue += prepend; | ||||
|         newValue += value.slice(selectionStart, selectionEnd); | ||||
|         newValue += append; | ||||
|         newValue += value.slice(selectionEnd); | ||||
|         return { prependLength: prepend.length, transformedValue: newValue }; | ||||
|       }); | ||||
|     }, | ||||
|     [transformationWrapper] | ||||
|   ); | ||||
| 
 | ||||
|   const unwrap = useCallback( | ||||
|     (wrapper: string) => { | ||||
|       transformationWrapper((value, selectionStart, selectionEnd) => { | ||||
|         let newValue = ""; | ||||
|         newValue += value.slice(0, selectionStart - wrapper.length); | ||||
|         newValue += value.slice(selectionStart, selectionEnd); | ||||
|         newValue += value.slice(wrapper.length + selectionEnd); | ||||
|         return { prependLength: -wrapper.length, transformedValue: newValue }; | ||||
|       }); | ||||
|     }, | ||||
|     [transformationWrapper] | ||||
|   ); | ||||
| 
 | ||||
|   const toggleWrap = useCallback( | ||||
|     ( | ||||
|       wrapper: string, | ||||
|       properties?: Record<string, string>, | ||||
|       addInnerNewLines?: boolean | ||||
|     ) => { | ||||
|       const textarea = | ||||
|         document.querySelector<HTMLTextAreaElement>("#editorTextArea"); | ||||
|       if (textarea) { | ||||
|         const { value, selectionStart, selectionEnd } = textarea; | ||||
| 
 | ||||
|         if ( | ||||
|           value.slice(selectionStart - wrapper.length, selectionStart) === | ||||
|             wrapper && | ||||
|           value.slice(selectionEnd, selectionEnd + wrapper.length) === wrapper | ||||
|         ) { | ||||
|           unwrap(wrapper); | ||||
|         } else { | ||||
|           wrap(wrapper, properties, addInnerNewLines); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     [unwrap, wrap] | ||||
|   ); | ||||
| 
 | ||||
|       let newValue = ""; | ||||
|       newValue += value.slice(0, selectionStart); | ||||
|       newValue += prepend; | ||||
|       newValue += value.slice(selectionStart, selectionEnd); | ||||
|       newValue += append; | ||||
|       newValue += value.slice(selectionEnd); | ||||
|       return { prependLength: prepend.length, transformedValue: newValue }; | ||||
|     }); | ||||
|   } | ||||
|   const preline = useCallback( | ||||
|     (prepend: string) => { | ||||
|       transformationWrapper((value, selectionStart) => { | ||||
|         const lastNewLine = | ||||
|           value.slice(0, selectionStart).lastIndexOf("\n") + 1; | ||||
| 
 | ||||
|   function toggleWrap( | ||||
|     wrapper: string, | ||||
|     properties?: Record<string, string>, | ||||
|     addInnerNewLines?: boolean | ||||
|   ) { | ||||
|     const textarea = | ||||
|       document.querySelector<HTMLTextAreaElement>("#editorTextArea"); | ||||
|     if (textarea) { | ||||
|       const { value, selectionStart, selectionEnd } = textarea; | ||||
|         let newValue = ""; | ||||
|         newValue += value.slice(0, lastNewLine); | ||||
|         newValue += prepend; | ||||
|         newValue += value.slice(lastNewLine); | ||||
| 
 | ||||
|       if ( | ||||
|         value.slice(selectionStart - wrapper.length, selectionStart) === | ||||
|           wrapper && | ||||
|         value.slice(selectionEnd, selectionEnd + wrapper.length) === wrapper | ||||
|       ) { | ||||
|         unwrap(wrapper); | ||||
|       } else { | ||||
|         wrap(wrapper, properties, addInnerNewLines); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|         return { prependLength: prepend.length, transformedValue: newValue }; | ||||
|       }); | ||||
|     }, | ||||
|     [transformationWrapper] | ||||
|   ); | ||||
| 
 | ||||
|   function unwrap(wrapper: string) { | ||||
|     transformationWrapper((value, selectionStart, selectionEnd) => { | ||||
|       let newValue = ""; | ||||
|       newValue += value.slice(0, selectionStart - wrapper.length); | ||||
|       newValue += value.slice(selectionStart, selectionEnd); | ||||
|       newValue += value.slice(wrapper.length + selectionEnd); | ||||
|       return { prependLength: -wrapper.length, transformedValue: newValue }; | ||||
|     }); | ||||
|   } | ||||
|   const insert = useCallback( | ||||
|     (prepend: string) => { | ||||
|       transformationWrapper((value, selectionStart) => { | ||||
|         let newValue = ""; | ||||
|         newValue += value.slice(0, selectionStart); | ||||
|         newValue += prepend; | ||||
|         newValue += value.slice(selectionStart); | ||||
| 
 | ||||
|   function preline(prepend: string) { | ||||
|     transformationWrapper((value, selectionStart) => { | ||||
|       const lastNewLine = value.slice(0, selectionStart).lastIndexOf("\n") + 1; | ||||
|         return { prependLength: prepend.length, transformedValue: newValue }; | ||||
|       }); | ||||
|     }, | ||||
|     [transformationWrapper] | ||||
|   ); | ||||
| 
 | ||||
|       let newValue = ""; | ||||
|       newValue += value.slice(0, lastNewLine); | ||||
|       newValue += prepend; | ||||
|       newValue += value.slice(lastNewLine); | ||||
|   const appendDoc = useCallback( | ||||
|     (append: string) => { | ||||
|       transformationWrapper((value) => { | ||||
|         const newValue = value + append; | ||||
|         return { prependLength: 0, transformedValue: newValue }; | ||||
|       }); | ||||
|     }, | ||||
|     [transformationWrapper] | ||||
|   ); | ||||
| 
 | ||||
|       return { prependLength: prepend.length, transformedValue: newValue }; | ||||
|     }); | ||||
|   } | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         <Popup setState={setConverterOpened} state={converterOpened}> | ||||
|           <div className="text-center"> | ||||
|             <h2 className="mt-4">Convert HTML to markdown</h2> | ||||
|             <p> | ||||
|               Copy and paste any HTML content (content from web pages) here. | ||||
|               <br /> | ||||
|               The text will immediatly be converted to valid Markdown. | ||||
|               <br /> | ||||
|               You can then copy the converted text and paste it anywhere you | ||||
|               want in the editor | ||||
|             </p> | ||||
|           </div> | ||||
|           <textarea | ||||
|             readOnly | ||||
|             id="htmlMdTextArea" | ||||
|             title="Ouput textarea" | ||||
|             onPaste={(event) => { | ||||
|               const turndownService = new TurndownService({ | ||||
|                 headingStyle: "atx", | ||||
|                 codeBlockStyle: "fenced", | ||||
|                 bulletListMarker: "-", | ||||
|                 emDelimiter: "_", | ||||
|                 strongDelimiter: "**", | ||||
|               }); | ||||
| 
 | ||||
|   function insert(prepend: string) { | ||||
|     transformationWrapper((value, selectionStart) => { | ||||
|       let newValue = ""; | ||||
|       newValue += value.slice(0, selectionStart); | ||||
|       newValue += prepend; | ||||
|       newValue += value.slice(selectionStart); | ||||
|               let paste = event.clipboardData.getData("text/html"); | ||||
|               paste = paste.replace(/<!--.*?-->/u, ""); | ||||
|               paste = turndownService.turndown(paste); | ||||
|               paste = paste.replace(/<!--.*?-->/u, ""); | ||||
| 
 | ||||
|       return { prependLength: prepend.length, transformedValue: newValue }; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function appendDoc(append: string) { | ||||
|     transformationWrapper((value) => { | ||||
|       const newValue = value + append; | ||||
|       return { prependLength: 0, transformedValue: newValue }; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function transformationWrapper( | ||||
|     transformation: ( | ||||
|       value: string, | ||||
|       selectionStart: number, | ||||
|       selectedEnd: number | ||||
|     ) => { prependLength: number; transformedValue: string } | ||||
|   ) { | ||||
|     const textarea = | ||||
|       document.querySelector<HTMLTextAreaElement>("#editorTextArea"); | ||||
|     if (textarea) { | ||||
|       const { value, selectionStart, selectionEnd } = textarea; | ||||
| 
 | ||||
|       const { prependLength, transformedValue } = transformation( | ||||
|         value, | ||||
|         selectionStart, | ||||
|         selectionEnd | ||||
|       ); | ||||
| 
 | ||||
|       textarea.value = transformedValue; | ||||
|       handleInput(textarea.value); | ||||
| 
 | ||||
|       textarea.focus(); | ||||
|       textarea.selectionStart = selectionStart + prependLength; | ||||
|       textarea.selectionEnd = selectionEnd + prependLength; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       <Popup setState={setConverterOpened} state={converterOpened}> | ||||
|         <div className="text-center"> | ||||
|           <h2 className="mt-4">Convert HTML to markdown</h2> | ||||
|           <p> | ||||
|             Copy and paste any HTML content (content from web pages) here.{" "} | ||||
|             <br /> | ||||
|             The text will immediatly be converted to valid Markdown. | ||||
|             <br /> | ||||
|             You can then copy the converted text and paste it anywhere you want | ||||
|             in the editor | ||||
|           </p> | ||||
|         </div> | ||||
|         <textarea | ||||
|           readOnly | ||||
|           id="htmlMdTextArea" | ||||
|           title="Ouput textarea" | ||||
|           onPaste={(event) => { | ||||
|             const turndownService = new TurndownService({ | ||||
|               headingStyle: "atx", | ||||
|               codeBlockStyle: "fenced", | ||||
|               bulletListMarker: "-", | ||||
|               emDelimiter: "_", | ||||
|               strongDelimiter: "**", | ||||
|             }); | ||||
| 
 | ||||
|             let paste = event.clipboardData.getData("text/html"); | ||||
|             paste = paste.replace(/<!--.*?-->/u, ""); | ||||
|             paste = turndownService.turndown(paste); | ||||
|             paste = paste.replace(/<!--.*?-->/u, ""); | ||||
| 
 | ||||
|             const target = event.target as HTMLTextAreaElement; | ||||
|             target.value = paste; | ||||
|             target.select(); | ||||
|             event.preventDefault(); | ||||
|           }} | ||||
|           className="h-[50vh] w-[50vw] mobile:w-[75vw]" | ||||
|         /> | ||||
|       </Popup> | ||||
| 
 | ||||
|       <div className="mb-4 flex flex-row gap-2"> | ||||
|         <ToolTip | ||||
|           content={ | ||||
|             <div className="grid gap-2"> | ||||
|               <h3 className="text-lg">Headers</h3> | ||||
|               <Button onClick={() => preline("# ")} text={"H1"} /> | ||||
|               <Button onClick={() => preline("## ")} text={"H2"} /> | ||||
|               <Button onClick={() => preline("### ")} text={"H3"} /> | ||||
|               <Button onClick={() => preline("#### ")} text={"H4"} /> | ||||
|               <Button onClick={() => preline("##### ")} text={"H5"} /> | ||||
|               <Button onClick={() => preline("###### ")} text={"H6"} /> | ||||
|             </div> | ||||
|           } | ||||
|         > | ||||
|           <Button icon={Icon.Title} /> | ||||
|         </ToolTip> | ||||
| 
 | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={<h3 className="text-lg">Toggle Bold</h3>} | ||||
|         > | ||||
|           <Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} /> | ||||
|         </ToolTip> | ||||
| 
 | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={<h3 className="text-lg">Toggle Italic</h3>} | ||||
|         > | ||||
|           <Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} /> | ||||
|         </ToolTip> | ||||
| 
 | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={ | ||||
|             <> | ||||
|               <h3 className="text-lg">Toggle Inline Code</h3> | ||||
|               <p> | ||||
|                 Makes the text monospace (like text from a computer terminal). | ||||
|                 Usually used for stylistic purposes in transcripts. | ||||
|               </p> | ||||
|             </> | ||||
|           } | ||||
|         > | ||||
|           <Button onClick={() => toggleWrap("`")} icon={Icon.Code} /> | ||||
|         </ToolTip> | ||||
| 
 | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={ | ||||
|             <> | ||||
|               <h3 className="text-lg">Insert footnote</h3> | ||||
|               <p>When inserted “x”</p> | ||||
|             </> | ||||
|           } | ||||
|         > | ||||
|           <Button | ||||
|             onClick={() => { | ||||
|               insert("[^x]"); | ||||
|               appendDoc("\n\n[^x]: This is a footnote."); | ||||
|               const target = event.target as HTMLTextAreaElement; | ||||
|               target.value = paste; | ||||
|               target.select(); | ||||
|               event.preventDefault(); | ||||
|             }} | ||||
|             icon={Icon.Superscript} | ||||
|             className="h-[50vh] w-[50vw] mobile:w-[75vw]" | ||||
|           /> | ||||
|         </ToolTip> | ||||
|         </Popup> | ||||
| 
 | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={ | ||||
|             <> | ||||
|               <h3 className="text-lg">Transcripts</h3> | ||||
|               <p> | ||||
|                 Use this to create dialogues and transcripts. Start by adding a | ||||
|                 container, then add transcript speech line within. | ||||
|               </p> | ||||
|         <div className="mb-4 flex flex-row gap-2"> | ||||
|           <ToolTip | ||||
|             content={ | ||||
|               <div className="grid gap-2"> | ||||
|                 <h3 className="text-lg">Headers</h3> | ||||
|                 <Button onClick={() => preline("# ")} text={"H1"} /> | ||||
|                 <Button onClick={() => preline("## ")} text={"H2"} /> | ||||
|                 <Button onClick={() => preline("### ")} text={"H3"} /> | ||||
|                 <Button onClick={() => preline("#### ")} text={"H4"} /> | ||||
|                 <Button onClick={() => preline("##### ")} text={"H5"} /> | ||||
|                 <Button onClick={() => preline("###### ")} text={"H6"} /> | ||||
|               </div> | ||||
|             } | ||||
|           > | ||||
|             <Button icon={Icon.Title} /> | ||||
|           </ToolTip> | ||||
| 
 | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={<h3 className="text-lg">Toggle Bold</h3>} | ||||
|           > | ||||
|             <Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} /> | ||||
|           </ToolTip> | ||||
| 
 | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={<h3 className="text-lg">Toggle Italic</h3>} | ||||
|           > | ||||
|             <Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} /> | ||||
|           </ToolTip> | ||||
| 
 | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={ | ||||
|               <> | ||||
|                 <h3 className="text-lg">Toggle Inline Code</h3> | ||||
|                 <p> | ||||
|                   Makes the text monospace (like text from a computer terminal). | ||||
|                   Usually used for stylistic purposes in transcripts. | ||||
|                 </p> | ||||
|               </> | ||||
|             } | ||||
|           > | ||||
|             <Button onClick={() => toggleWrap("`")} icon={Icon.Code} /> | ||||
|           </ToolTip> | ||||
| 
 | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={ | ||||
|               <> | ||||
|                 <h3 className="text-lg">Insert footnote</h3> | ||||
|                 <p>When inserted “x”</p> | ||||
|               </> | ||||
|             } | ||||
|           > | ||||
|             <Button | ||||
|               onClick={() => { | ||||
|                 insert("[^x]"); | ||||
|                 appendDoc("\n\n[^x]: This is a footnote."); | ||||
|               }} | ||||
|               icon={Icon.Superscript} | ||||
|             /> | ||||
|           </ToolTip> | ||||
| 
 | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={ | ||||
|               <> | ||||
|                 <h3 className="text-lg">Transcripts</h3> | ||||
|                 <p> | ||||
|                   Use this to create dialogues and transcripts. Start by adding | ||||
|                   a container, then add transcript speech line within. | ||||
|                 </p> | ||||
|                 <div className="grid gap-2"> | ||||
|                   <ToolTip | ||||
|                     placement="right" | ||||
|                     content={ | ||||
|                       <> | ||||
|                         <h3 className="text-lg">Transcript container</h3> | ||||
|                       </> | ||||
|                     } | ||||
|                   > | ||||
|                     <Button | ||||
|                       onClick={() => wrap("Transcript", {}, true)} | ||||
|                       icon={Icon.AddBox} | ||||
|                     /> | ||||
|                   </ToolTip> | ||||
|                   <ToolTip | ||||
|                     placement="right" | ||||
|                     content={ | ||||
|                       <> | ||||
|                         <h3 className="text-lg">Transcript speech line</h3> | ||||
|                         <p> | ||||
|                           Use to add a dialogue/transcript line. Change the{" "} | ||||
|                           <kbd>name</kbd> property to chang the name of the | ||||
|                           speaker | ||||
|                         </p> | ||||
|                       </> | ||||
|                     } | ||||
|                   > | ||||
|                     <Button | ||||
|                       onClick={() => wrap("Line", { name: "speaker" })} | ||||
|                       icon={Icon.RecordVoiceOver} | ||||
|                     /> | ||||
|                   </ToolTip> | ||||
|                 </div> | ||||
|               </> | ||||
|             } | ||||
|           > | ||||
|             <Button icon={Icon.RecordVoiceOver} /> | ||||
|           </ToolTip> | ||||
| 
 | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={<h3 className="text-lg">Inset box</h3>} | ||||
|           > | ||||
|             <Button | ||||
|               onClick={() => wrap("InsetBox", {}, true)} | ||||
|               icon={Icon.CheckBoxOutlineBlank} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={<h3 className="text-lg">Scene break</h3>} | ||||
|           > | ||||
|             <Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} /> | ||||
|           </ToolTip> | ||||
|           <ToolTip | ||||
|             content={ | ||||
|               <div className="flex flex-col place-items-center gap-2"> | ||||
|                 <h3 className="text-lg">Links</h3> | ||||
|                 <ToolTip | ||||
|                   placement="right" | ||||
|                   content={ | ||||
|                     <> | ||||
|                       <h3 className="text-lg">Transcript container</h3> | ||||
|                     </> | ||||
|                   } | ||||
|                 > | ||||
|                   <Button | ||||
|                     onClick={() => wrap("Transcript", {}, true)} | ||||
|                     icon={Icon.AddBox} | ||||
|                   /> | ||||
|                 </ToolTip> | ||||
|                 <ToolTip | ||||
|                   placement="right" | ||||
|                   content={ | ||||
|                     <> | ||||
|                       <h3 className="text-lg">Transcript speech line</h3> | ||||
|                       <p> | ||||
|                         Use to add a dialogue/transcript line. Change the{" "} | ||||
|                         <kbd>name</kbd> property to chang the name of the | ||||
|                         speaker | ||||
|                       <h3 className="text-lg">External Link</h3> | ||||
|                       <p className="text-xs"> | ||||
|                         Provides a link to another webpage / website | ||||
|                       </p> | ||||
|                     </> | ||||
|                   } | ||||
|                 > | ||||
|                   <Button | ||||
|                     onClick={() => wrap("Line", { name: "speaker" })} | ||||
|                     icon={Icon.RecordVoiceOver} | ||||
|                     onClick={() => insert("[Link name](https://domain.com)")} | ||||
|                     icon={Icon.Link} | ||||
|                     text={"External"} | ||||
|                   /> | ||||
|                 </ToolTip> | ||||
| 
 | ||||
|                 <ToolTip | ||||
|                   placement="right" | ||||
|                   content={ | ||||
|                     <> | ||||
|                       <h3 className="text-lg">Intralink</h3> | ||||
|                       <p className="text-xs"> | ||||
|                         Interlinks are used to add links to a header within the | ||||
|                         same document | ||||
|                       </p> | ||||
|                     </> | ||||
|                   } | ||||
|                 > | ||||
|                   <Button | ||||
|                     onClick={() => wrap("IntraLink", {})} | ||||
|                     icon={Icon.Link} | ||||
|                     text={"Internal"} | ||||
|                   /> | ||||
|                 </ToolTip> | ||||
|                 <ToolTip | ||||
|                   placement="right" | ||||
|                   content={ | ||||
|                     <> | ||||
|                       <h3 className="text-lg">Intralink (with target)</h3>{" "} | ||||
|                       <p className="text-xs"> | ||||
|                         Use this one if you want the intralink text to be | ||||
|                         different from the target header’s name. | ||||
|                       </p> | ||||
|                     </> | ||||
|                   } | ||||
|                 > | ||||
|                   <Button | ||||
|                     onClick={() => wrap("IntraLink", { target: "target" })} | ||||
|                     icon={Icon.Link} | ||||
|                     text="Internal (w/ target)" | ||||
|                   /> | ||||
|                 </ToolTip> | ||||
|               </div> | ||||
|             </> | ||||
|           } | ||||
|         > | ||||
|           <Button icon={Icon.RecordVoiceOver} /> | ||||
|         </ToolTip> | ||||
|             } | ||||
|           > | ||||
|             <Button icon={Icon.Link} /> | ||||
|           </ToolTip> | ||||
| 
 | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={<h3 className="text-lg">Inset box</h3>} | ||||
|         > | ||||
|           <Button | ||||
|             onClick={() => wrap("InsetBox", {}, true)} | ||||
|             icon={Icon.CheckBoxOutlineBlank} | ||||
|           /> | ||||
|         </ToolTip> | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={<h3 className="text-lg">Scene break</h3>} | ||||
|         > | ||||
|           <Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} /> | ||||
|         </ToolTip> | ||||
|         <ToolTip | ||||
|           content={ | ||||
|             <div className="flex flex-col place-items-center gap-2"> | ||||
|               <h3 className="text-lg">Links</h3> | ||||
|               <ToolTip | ||||
|                 placement="right" | ||||
|                 content={ | ||||
|                   <> | ||||
|                     <h3 className="text-lg">External Link</h3> | ||||
|                     <p className="text-xs"> | ||||
|                       Provides a link to another webpage / website | ||||
|                     </p> | ||||
|                   </> | ||||
|                 } | ||||
|               > | ||||
|                 <Button | ||||
|                   onClick={() => insert("[Link name](https://domain.com)")} | ||||
|                   icon={Icon.Link} | ||||
|                   text={"External"} | ||||
|                 /> | ||||
|               </ToolTip> | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={ | ||||
|               <h3 className="text-lg">Player’s name placeholder</h3> | ||||
|             } | ||||
|           > | ||||
|             <Button onClick={() => insert("<player>")} icon={Icon.Person} /> | ||||
|           </ToolTip> | ||||
| 
 | ||||
|               <ToolTip | ||||
|                 placement="right" | ||||
|                 content={ | ||||
|                   <> | ||||
|                     <h3 className="text-lg">Intralink</h3> | ||||
|                     <p className="text-xs"> | ||||
|                       Interlinks are used to add links to a header within the | ||||
|                       same document | ||||
|                     </p> | ||||
|                   </> | ||||
|                 } | ||||
|               > | ||||
|                 <Button | ||||
|                   onClick={() => wrap("IntraLink", {})} | ||||
|                   icon={Icon.Link} | ||||
|                   text={"Internal"} | ||||
|                 /> | ||||
|               </ToolTip> | ||||
|               <ToolTip | ||||
|                 placement="right" | ||||
|                 content={ | ||||
|                   <> | ||||
|                     <h3 className="text-lg">Intralink (with target)</h3>{" "} | ||||
|                     <p className="text-xs"> | ||||
|                       Use this one if you want the intralink text to be | ||||
|                       different from the target header’s name. | ||||
|                     </p> | ||||
|                   </> | ||||
|                 } | ||||
|               > | ||||
|                 <Button | ||||
|                   onClick={() => wrap("IntraLink", { target: "target" })} | ||||
|                   icon={Icon.Link} | ||||
|                   text="Internal (w/ target)" | ||||
|                 /> | ||||
|               </ToolTip> | ||||
|             </div> | ||||
|           } | ||||
|         > | ||||
|           <Button icon={Icon.Link} /> | ||||
|         </ToolTip> | ||||
| 
 | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={<h3 className="text-lg">Player’s name placeholder</h3>} | ||||
|         > | ||||
|           <Button onClick={() => insert("<player>")} icon={Icon.Person} /> | ||||
|         </ToolTip> | ||||
| 
 | ||||
|         <ToolTip | ||||
|           placement="bottom" | ||||
|           content={<h3 className="text-lg">Open HTML Converter</h3>} | ||||
|         > | ||||
|           <Button | ||||
|             onClick={() => { | ||||
|               setConverterOpened(true); | ||||
|             }} | ||||
|             icon={Icon.Html} | ||||
|           /> | ||||
|         </ToolTip> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="grid grid-cols-2 gap-8"> | ||||
|         <div> | ||||
|           <h2>Editor</h2> | ||||
|           <textarea | ||||
|             id="editorTextArea" | ||||
|             onInput={(event) => { | ||||
|               const textarea = event.target as HTMLTextAreaElement; | ||||
|               handleInput(textarea.value); | ||||
|             }} | ||||
|             className="h-[70vh] w-full rounded-xl bg-mid !bg-opacity-40 p-8 | ||||
|             font-mono text-black outline-none" | ||||
|             value={markdown} | ||||
|             title="Input textarea" | ||||
|           /> | ||||
|           <ToolTip | ||||
|             placement="bottom" | ||||
|             content={<h3 className="text-lg">Open HTML Converter</h3>} | ||||
|           > | ||||
|             <Button | ||||
|               onClick={() => { | ||||
|                 setConverterOpened(true); | ||||
|               }} | ||||
|               icon={Icon.Html} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|         </div> | ||||
|         <div> | ||||
|           <h2>Preview</h2> | ||||
|           <div className="h-[70vh] overflow-scroll rounded-xl bg-mid bg-opacity-40 p-8"> | ||||
|             <Markdawn className="w-full" text={markdown} /> | ||||
| 
 | ||||
|         <div className="grid grid-cols-2 gap-8"> | ||||
|           <div> | ||||
|             <h2>Editor</h2> | ||||
|             <textarea | ||||
|               id="editorTextArea" | ||||
|               onInput={(event) => { | ||||
|                 const textarea = event.target as HTMLTextAreaElement; | ||||
|                 handleInput(textarea.value); | ||||
|               }} | ||||
|               className="h-[70vh] w-full rounded-xl bg-mid !bg-opacity-40 p-8 | ||||
|             font-mono text-black outline-none" | ||||
|               value={markdown} | ||||
|               title="Input textarea" | ||||
|             /> | ||||
|           </div> | ||||
|           <div> | ||||
|             <h2>Preview</h2> | ||||
|             <div className="h-[70vh] overflow-scroll rounded-xl bg-mid bg-opacity-40 p-8"> | ||||
|               <Markdawn className="w-full" text={markdown} /> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="mt-8"> | ||||
|         <TOC text={markdown} /> | ||||
|       </div> | ||||
|     </ContentPanel> | ||||
|         <div className="mt-8"> | ||||
|           <TOC text={markdown} /> | ||||
|         </div> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       appendDoc, | ||||
|       converterOpened, | ||||
|       handleInput, | ||||
|       insert, | ||||
|       markdown, | ||||
|       preline, | ||||
|       toggleWrap, | ||||
|       wrap, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout | ||||
|       navTitle="Markdawn Editor" | ||||
|  | ||||
| @ -49,15 +49,17 @@ import { | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { Fragment, useState } from "react"; | ||||
| import { Fragment, useMemo, useState } from "react"; | ||||
| import { isUntangibleGroupItem } from "helpers/libraryItem"; | ||||
| import { useMediaHoverable } from "hooks/useMediaQuery"; | ||||
| import { WithLabel } from "components/Inputs/WithLabel"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   item: NonNullable< | ||||
|     GetLibraryItemQuery["libraryItems"] | ||||
|   >["data"][number]["attributes"]; | ||||
|     NonNullable< | ||||
|       GetLibraryItemQuery["libraryItems"] | ||||
|     >["data"][number]["attributes"] | ||||
|   >; | ||||
|   itemId: NonNullable< | ||||
|     GetLibraryItemQuery["libraryItems"] | ||||
|   >["data"][number]["id"]; | ||||
| @ -67,448 +69,474 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|   const { item, itemId, langui, currencies } = props; | ||||
|   const appLayout = useAppLayout(); | ||||
|   const hoverable = useMediaHoverable(); | ||||
| 
 | ||||
|   useScrollTopOnChange(AnchorIds.ContentPanel, [item]); | ||||
| 
 | ||||
|   const isVariantSet = | ||||
|     item?.metadata?.[0]?.__typename === "ComponentMetadataGroup" && | ||||
|     item.metadata[0].subtype?.data?.attributes?.slug === "variant-set"; | ||||
| 
 | ||||
|   sortContent(item?.contents); | ||||
| 
 | ||||
|   const [openLightBox, LightBox] = useLightBox(); | ||||
|   const [keepInfoVisible, setKeepInfoVisible] = useState(false); | ||||
| 
 | ||||
|   let displayOpenScans = false; | ||||
|   if (item?.contents?.data) | ||||
|     for (const content of item.contents.data) { | ||||
|       if ( | ||||
|         content.attributes?.scan_set && | ||||
|         content.attributes.scan_set.length > 0 | ||||
|       ) | ||||
|         displayOpenScans = true; | ||||
|     } | ||||
|   useScrollTopOnChange(AnchorIds.ContentPanel, [item]); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href="/library/" | ||||
|         title={langui.library} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Desktop} | ||||
|         horizontalLine | ||||
|       /> | ||||
| 
 | ||||
|       <div className="grid gap-4"> | ||||
|         <NavOption title={langui.summary} url="#summary" border /> | ||||
| 
 | ||||
|         {item?.gallery && item.gallery.data.length > 0 && ( | ||||
|           <NavOption title={langui.gallery} url="#gallery" border /> | ||||
|         )} | ||||
| 
 | ||||
|         <NavOption title={langui.details} url="#details" border /> | ||||
| 
 | ||||
|         {item?.subitems && item.subitems.data.length > 0 && ( | ||||
|           <NavOption | ||||
|             title={isVariantSet ? langui.variants : langui.subitems} | ||||
|             url={isVariantSet ? "#variants" : "#subitems"} | ||||
|             border | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
|         {item?.contents && item.contents.data.length > 0 && ( | ||||
|           <NavOption title={langui.contents} url="#contents" border /> | ||||
|         )} | ||||
|       </div> | ||||
|     </SubPanel> | ||||
|   const isVariantSet = useMemo( | ||||
|     () => | ||||
|       item.metadata?.[0]?.__typename === "ComponentMetadataGroup" && | ||||
|       item.metadata[0].subtype?.data?.attributes?.slug === "variant-set", | ||||
|     [item.metadata] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       <LightBox /> | ||||
|   const displayOpenScans = useMemo( | ||||
|     () => | ||||
|       item.contents?.data.some( | ||||
|         (content) => | ||||
|           content.attributes?.scan_set && content.attributes.scan_set.length > 0 | ||||
|       ), | ||||
|     [item.contents?.data] | ||||
|   ); | ||||
| 
 | ||||
|       <ReturnButton | ||||
|         href="/library/" | ||||
|         title={langui.library} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Mobile} | ||||
|         className="mb-10" | ||||
|       /> | ||||
|       <div className="grid place-items-center gap-12"> | ||||
|         <div | ||||
|           className="relative h-[50vh] w-full | ||||
|           cursor-pointer drop-shadow-shade-xl desktop:mb-16 mobile:h-[60vh]" | ||||
|           onClick={() => { | ||||
|             if (item?.thumbnail?.data?.attributes) { | ||||
|               openLightBox([ | ||||
|                 getAssetURL( | ||||
|                   item.thumbnail.data.attributes.url, | ||||
|                   ImageQuality.Large | ||||
|                 ), | ||||
|               ]); | ||||
|             } | ||||
|           }} | ||||
|         > | ||||
|           {item?.thumbnail?.data?.attributes ? ( | ||||
|             <Img | ||||
|               image={item.thumbnail.data.attributes} | ||||
|               quality={ImageQuality.Large} | ||||
|               className="h-full w-full object-contain" | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <ReturnButton | ||||
|           href="/library/" | ||||
|           title={langui.library} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Desktop} | ||||
|           horizontalLine | ||||
|         /> | ||||
| 
 | ||||
|         <div className="grid gap-4"> | ||||
|           <NavOption title={langui.summary} url="#summary" border /> | ||||
| 
 | ||||
|           {item.gallery && item.gallery.data.length > 0 && ( | ||||
|             <NavOption title={langui.gallery} url="#gallery" border /> | ||||
|           )} | ||||
| 
 | ||||
|           <NavOption title={langui.details} url="#details" border /> | ||||
| 
 | ||||
|           {item.subitems && item.subitems.data.length > 0 && ( | ||||
|             <NavOption | ||||
|               title={isVariantSet ? langui.variants : langui.subitems} | ||||
|               url={isVariantSet ? "#variants" : "#subitems"} | ||||
|               border | ||||
|             /> | ||||
|           ) : ( | ||||
|             <div className="aspect-[21/29.7] w-full rounded-xl bg-light"></div> | ||||
|           )} | ||||
| 
 | ||||
|           {item.contents && item.contents.data.length > 0 && ( | ||||
|             <NavOption title={langui.contents} url="#contents" border /> | ||||
|           )} | ||||
|         </div> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [isVariantSet, item.contents, item.gallery, item.subitems, langui] | ||||
|   ); | ||||
| 
 | ||||
|         <InsetBox id="summary" className="grid place-items-center"> | ||||
|           <div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8"> | ||||
|             {item?.subitem_of?.data[0]?.attributes && ( | ||||
|               <div className="grid place-items-center"> | ||||
|                 <p>{langui.subitem_of}</p> | ||||
|                 <Button | ||||
|                   href={`/library/${item.subitem_of.data[0].attributes.slug}`} | ||||
|                   text={prettyinlineTitle( | ||||
|                     "", | ||||
|                     item.subitem_of.data[0].attributes.title, | ||||
|                     item.subitem_of.data[0].attributes.subtitle | ||||
|                   )} | ||||
|                 /> | ||||
|               </div> | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         <LightBox /> | ||||
| 
 | ||||
|         <ReturnButton | ||||
|           href="/library/" | ||||
|           title={langui.library} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Mobile} | ||||
|           className="mb-10" | ||||
|         /> | ||||
|         <div className="grid place-items-center gap-12"> | ||||
|           <div | ||||
|             className="relative h-[50vh] w-full | ||||
|           cursor-pointer drop-shadow-shade-xl desktop:mb-16 mobile:h-[60vh]" | ||||
|             onClick={() => { | ||||
|               if (item.thumbnail?.data?.attributes) { | ||||
|                 openLightBox([ | ||||
|                   getAssetURL( | ||||
|                     item.thumbnail.data.attributes.url, | ||||
|                     ImageQuality.Large | ||||
|                   ), | ||||
|                 ]); | ||||
|               } | ||||
|             }} | ||||
|           > | ||||
|             {item.thumbnail?.data?.attributes ? ( | ||||
|               <Img | ||||
|                 image={item.thumbnail.data.attributes} | ||||
|                 quality={ImageQuality.Large} | ||||
|                 className="h-full w-full object-contain" | ||||
|               /> | ||||
|             ) : ( | ||||
|               <div className="aspect-[21/29.7] w-full rounded-xl bg-light"></div> | ||||
|             )} | ||||
|             <div className="grid place-items-center text-center"> | ||||
|               <h1 className="text-3xl">{item?.title}</h1> | ||||
|               {item && isDefinedAndNotEmpty(item.subtitle) && ( | ||||
|                 <h2 className="text-2xl">{item.subtitle}</h2> | ||||
|           </div> | ||||
| 
 | ||||
|           <InsetBox id="summary" className="grid place-items-center"> | ||||
|             <div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8"> | ||||
|               {item.subitem_of?.data[0]?.attributes && ( | ||||
|                 <div className="grid place-items-center"> | ||||
|                   <p>{langui.subitem_of}</p> | ||||
|                   <Button | ||||
|                     href={`/library/${item.subitem_of.data[0].attributes.slug}`} | ||||
|                     text={prettyinlineTitle( | ||||
|                       "", | ||||
|                       item.subitem_of.data[0].attributes.title, | ||||
|                       item.subitem_of.data[0].attributes.subtitle | ||||
|                     )} | ||||
|                   /> | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
| 
 | ||||
|             <PreviewCardCTAs | ||||
|               id={itemId} | ||||
|               displayCTAs={!isUntangibleGroupItem(item?.metadata?.[0])} | ||||
|               langui={langui} | ||||
|               expand | ||||
|             /> | ||||
|             {item?.descriptions?.[0] && ( | ||||
|               <p className="text-justify">{item.descriptions[0].description}</p> | ||||
|             )} | ||||
|             {!( | ||||
|               item?.metadata && | ||||
|               item.metadata[0]?.__typename === "ComponentMetadataGroup" && | ||||
|               (item.metadata[0].subtype?.data?.attributes?.slug === | ||||
|                 "variant-set" || | ||||
|                 item.metadata[0].subtype?.data?.attributes?.slug === | ||||
|                   "relation-set") | ||||
|             ) && ( | ||||
|               <> | ||||
|                 {item?.urls && item.urls.length ? ( | ||||
|                   <div className="flex flex-row place-items-center gap-3"> | ||||
|                     <p>{langui.available_at}</p> | ||||
|                     {filterHasAttributes(item.urls).map((url, index) => ( | ||||
|                       <Fragment key={index}> | ||||
|                         <Button | ||||
|                           href={url.url} | ||||
|                           target={"_blank"} | ||||
|                           text={prettyURL(url.url)} | ||||
|                         /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 ) : ( | ||||
|                   <p>{langui.item_not_available}</p> | ||||
|               <div className="grid place-items-center text-center"> | ||||
|                 <h1 className="text-3xl">{item.title}</h1> | ||||
|                 {isDefinedAndNotEmpty(item.subtitle) && ( | ||||
|                   <h2 className="text-2xl">{item.subtitle}</h2> | ||||
|                 )} | ||||
|               </> | ||||
|             )} | ||||
|           </div> | ||||
|         </InsetBox> | ||||
| 
 | ||||
|         {item?.gallery && item.gallery.data.length > 0 && ( | ||||
|           <div id="gallery" className="grid w-full place-items-center  gap-8"> | ||||
|             <h2 className="text-2xl">{langui.gallery}</h2> | ||||
|             <div | ||||
|               className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end | ||||
|               gap-8" | ||||
|             > | ||||
|               {filterHasAttributes(item.gallery.data).map( | ||||
|                 (galleryItem, index) => ( | ||||
|                   <Fragment key={galleryItem.id}> | ||||
|                     <div | ||||
|                       className="relative aspect-square cursor-pointer | ||||
|                       transition-transform hover:scale-[1.02]" | ||||
|                       onClick={() => { | ||||
|                         const images: string[] = filterHasAttributes( | ||||
|                           item.gallery?.data | ||||
|                         ).map((image) => | ||||
|                           getAssetURL(image.attributes.url, ImageQuality.Large) | ||||
|                         ); | ||||
|                         openLightBox(images, index); | ||||
|                       }} | ||||
|                     > | ||||
|                       <Img | ||||
|                         className="h-full w-full rounded-lg | ||||
|                         bg-light object-cover drop-shadow-shade-md" | ||||
|                         image={galleryItem.attributes} | ||||
|                       /> | ||||
|                     </div> | ||||
|                   </Fragment> | ||||
|                 ) | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
|         )} | ||||
| 
 | ||||
|         <InsetBox id="details" className="grid place-items-center"> | ||||
|           <div className="place-items grid w-[clamp(0px,100%,42rem)] gap-8"> | ||||
|             <h2 className="text-center text-2xl">{langui.details}</h2> | ||||
|             <div | ||||
|               className="grid place-items-center gap-y-8 | ||||
|               desktop:grid-flow-col desktop:place-content-between" | ||||
|             > | ||||
|               {item?.metadata?.[0] && ( | ||||
|                 <div className="grid place-content-start place-items-center"> | ||||
|                   <h3 className="text-xl">{langui.type}</h3> | ||||
|                   <div className="grid grid-flow-col gap-1"> | ||||
|                     <Chip>{prettyItemType(item.metadata[0], langui)}</Chip> | ||||
|                     {"›"} | ||||
|                     <Chip>{prettyItemSubType(item.metadata[0])}</Chip> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|               {item?.release_date && ( | ||||
|                 <div className="grid place-content-start place-items-center"> | ||||
|                   <h3 className="text-xl">{langui.release_date}</h3> | ||||
|                   <p>{prettyDate(item.release_date)}</p> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|               {item?.price && ( | ||||
|                 <div className="grid place-content-start place-items-center text-center"> | ||||
|                   <h3 className="text-xl">{langui.price}</h3> | ||||
|                   <p> | ||||
|                     {prettyPrice( | ||||
|                       item.price, | ||||
|                       currencies, | ||||
|                       item.price.currency?.data?.attributes?.code | ||||
|                     )} | ||||
|                   </p> | ||||
|                   {item.price.currency?.data?.attributes?.code !== | ||||
|                     appLayout.currency && ( | ||||
|                     <p> | ||||
|                       {prettyPrice(item.price, currencies, appLayout.currency)}{" "} | ||||
|                       <br />({langui.calculated?.toLowerCase()}) | ||||
|                     </p> | ||||
|                   )} | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
| 
 | ||||
|             {item?.categories && item.categories.data.length > 0 && ( | ||||
|               <div className="flex flex-col place-items-center gap-2"> | ||||
|                 <h3 className="text-xl">{langui.categories}</h3> | ||||
|                 <div className="flex flex-row flex-wrap place-content-center gap-2"> | ||||
|                   {item.categories.data.map((category) => ( | ||||
|                     <Chip key={category.id}>{category.attributes?.name}</Chip> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {item?.size && ( | ||||
|               <div className="grid gap-8 mobile:place-items-center"> | ||||
|                 <h3 className="text-xl">{langui.size}</h3> | ||||
|                 <div | ||||
|                   className="grid w-full grid-flow-col place-content-between thin:grid-flow-row | ||||
|                   thin:place-content-center thin:gap-8" | ||||
|                 > | ||||
|                   <div | ||||
|                     className="grid place-items-center gap-x-4 desktop:grid-flow-col  | ||||
|                     desktop:place-items-start" | ||||
|                   > | ||||
|                     <p className="font-bold">{langui.width}:</p> | ||||
|                     <div> | ||||
|                       <p>{item.size.width} mm</p> | ||||
|                       <p>{convertMmToInch(item.size.width)} in</p> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div | ||||
|                     className="grid place-items-center gap-x-4 desktop:grid-flow-col | ||||
|                     desktop:place-items-start" | ||||
|                   > | ||||
|                     <p className="font-bold">{langui.height}:</p> | ||||
|                     <div> | ||||
|                       <p>{item.size.height} mm</p> | ||||
|                       <p>{convertMmToInch(item.size.height)} in</p> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   {isDefined(item.size.thickness) && ( | ||||
|                     <div | ||||
|                       className="grid place-items-center gap-x-4 desktop:grid-flow-col | ||||
|                       desktop:place-items-start" | ||||
|                     > | ||||
|                       <p className="font-bold">{langui.thickness}:</p> | ||||
|                       <div> | ||||
|                         <p>{item.size.thickness} mm</p> | ||||
|                         <p>{convertMmToInch(item.size.thickness)} in</p> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                   )} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {item?.metadata?.[0]?.__typename !== "ComponentMetadataGroup" && | ||||
|               item?.metadata?.[0]?.__typename !== "ComponentMetadataOther" && ( | ||||
|               <PreviewCardCTAs | ||||
|                 id={itemId} | ||||
|                 displayCTAs={!isUntangibleGroupItem(item.metadata?.[0])} | ||||
|                 langui={langui} | ||||
|                 expand | ||||
|               /> | ||||
|               {item.descriptions?.[0] && ( | ||||
|                 <p className="text-justify"> | ||||
|                   {item.descriptions[0].description} | ||||
|                 </p> | ||||
|               )} | ||||
|               {!( | ||||
|                 item.metadata && | ||||
|                 item.metadata[0]?.__typename === "ComponentMetadataGroup" && | ||||
|                 (item.metadata[0].subtype?.data?.attributes?.slug === | ||||
|                   "variant-set" || | ||||
|                   item.metadata[0].subtype?.data?.attributes?.slug === | ||||
|                     "relation-set") | ||||
|               ) && ( | ||||
|                 <> | ||||
|                   <h3 className="text-xl">{langui.type_information}</h3> | ||||
|                   <div className="grid w-full grid-cols-2 place-content-between"> | ||||
|                     {item?.metadata?.[0]?.__typename === | ||||
|                       "ComponentMetadataBooks" && ( | ||||
|                       <> | ||||
|                         <div className="flex flex-row place-content-start gap-4"> | ||||
|                           <p className="font-bold">{langui.pages}:</p> | ||||
|                           <p>{item.metadata[0].page_count}</p> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div className="flex flex-row place-content-start gap-4"> | ||||
|                           <p className="font-bold">{langui.binding}:</p> | ||||
|                           <p> | ||||
|                             {item.metadata[0].binding_type === | ||||
|                             Enum_Componentmetadatabooks_Binding_Type.Paperback | ||||
|                               ? langui.paperback | ||||
|                               : item.metadata[0].binding_type === | ||||
|                                 Enum_Componentmetadatabooks_Binding_Type.Hardcover | ||||
|                               ? langui.hardcover | ||||
|                               : ""} | ||||
|                           </p> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div className="flex flex-row place-content-start gap-4"> | ||||
|                           <p className="font-bold">{langui.page_order}:</p> | ||||
|                           <p> | ||||
|                             {item.metadata[0].page_order === | ||||
|                             Enum_Componentmetadatabooks_Page_Order.LeftToRight | ||||
|                               ? langui.left_to_right | ||||
|                               : langui.right_to_left} | ||||
|                           </p> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div className="flex flex-row place-content-start gap-4"> | ||||
|                           <p className="font-bold">{langui.languages}:</p> | ||||
|                           {item.metadata[0]?.languages?.data.map((lang) => ( | ||||
|                             <p key={lang.attributes?.code}> | ||||
|                               {lang.attributes?.name} | ||||
|                             </p> | ||||
|                           ))} | ||||
|                         </div> | ||||
|                       </> | ||||
|                     )} | ||||
|                   </div> | ||||
|                   {item.urls?.length ? ( | ||||
|                     <div className="flex flex-row place-items-center gap-3"> | ||||
|                       <p>{langui.available_at}</p> | ||||
|                       {filterHasAttributes(item.urls).map((url, index) => ( | ||||
|                         <Fragment key={index}> | ||||
|                           <Button | ||||
|                             href={url.url} | ||||
|                             target={"_blank"} | ||||
|                             text={prettyURL(url.url)} | ||||
|                           /> | ||||
|                         </Fragment> | ||||
|                       ))} | ||||
|                     </div> | ||||
|                   ) : ( | ||||
|                     <p>{langui.item_not_available}</p> | ||||
|                   )} | ||||
|                 </> | ||||
|               )} | ||||
|           </div> | ||||
|         </InsetBox> | ||||
|             </div> | ||||
|           </InsetBox> | ||||
| 
 | ||||
|         {item?.subitems && item.subitems.data.length > 0 && ( | ||||
|           <div | ||||
|             id={isVariantSet ? "variants" : "subitems"} | ||||
|             className="grid w-full place-items-center gap-8" | ||||
|           > | ||||
|             <h2 className="text-2xl"> | ||||
|               {isVariantSet ? langui.variants : langui.subitems} | ||||
|             </h2> | ||||
|           {item.gallery && item.gallery.data.length > 0 && ( | ||||
|             <div id="gallery" className="grid w-full place-items-center  gap-8"> | ||||
|               <h2 className="text-2xl">{langui.gallery}</h2> | ||||
|               <div | ||||
|                 className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end | ||||
|               gap-8" | ||||
|               > | ||||
|                 {filterHasAttributes(item.gallery.data).map( | ||||
|                   (galleryItem, index) => ( | ||||
|                     <Fragment key={galleryItem.id}> | ||||
|                       <div | ||||
|                         className="relative aspect-square cursor-pointer | ||||
|                       transition-transform hover:scale-[1.02]" | ||||
|                         onClick={() => { | ||||
|                           const images: string[] = filterHasAttributes( | ||||
|                             item.gallery?.data | ||||
|                           ).map((image) => | ||||
|                             getAssetURL( | ||||
|                               image.attributes.url, | ||||
|                               ImageQuality.Large | ||||
|                             ) | ||||
|                           ); | ||||
|                           openLightBox(images, index); | ||||
|                         }} | ||||
|                       > | ||||
|                         <Img | ||||
|                           className="h-full w-full rounded-lg | ||||
|                         bg-light object-cover drop-shadow-shade-md" | ||||
|                           image={galleryItem.attributes} | ||||
|                         /> | ||||
|                       </div> | ||||
|                     </Fragment> | ||||
|                   ) | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
| 
 | ||||
|             {hoverable && ( | ||||
|               <WithLabel | ||||
|                 label={langui.always_show_info} | ||||
|                 input={ | ||||
|                   <Switch | ||||
|                     setState={setKeepInfoVisible} | ||||
|                     state={keepInfoVisible} | ||||
|                   /> | ||||
|                 } | ||||
|               /> | ||||
|             )} | ||||
|           <InsetBox id="details" className="grid place-items-center"> | ||||
|             <div className="place-items grid w-[clamp(0px,100%,42rem)] gap-8"> | ||||
|               <h2 className="text-center text-2xl">{langui.details}</h2> | ||||
|               <div | ||||
|                 className="grid place-items-center gap-y-8 | ||||
|               desktop:grid-flow-col desktop:place-content-between" | ||||
|               > | ||||
|                 {item.metadata?.[0] && ( | ||||
|                   <div className="grid place-content-start place-items-center"> | ||||
|                     <h3 className="text-xl">{langui.type}</h3> | ||||
|                     <div className="grid grid-flow-col gap-1"> | ||||
|                       <Chip>{prettyItemType(item.metadata[0], langui)}</Chip> | ||||
|                       {"›"} | ||||
|                       <Chip>{prettyItemSubType(item.metadata[0])}</Chip> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 )} | ||||
| 
 | ||||
|             <div | ||||
|               className="grid w-full grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] | ||||
|               items-end gap-8 mobile:grid-cols-2 thin:grid-cols-1" | ||||
|             > | ||||
|               {filterHasAttributes(item.subitems.data).map((subitem) => ( | ||||
|                 <Fragment key={subitem.id}> | ||||
|                   <PreviewCard | ||||
|                     href={`/library/${subitem.attributes.slug}`} | ||||
|                     title={subitem.attributes.title} | ||||
|                     subtitle={subitem.attributes.subtitle} | ||||
|                     thumbnail={subitem.attributes.thumbnail?.data?.attributes} | ||||
|                     thumbnailAspectRatio="21/29.7" | ||||
|                     thumbnailRounded={false} | ||||
|                     keepInfoVisible={keepInfoVisible} | ||||
|                     topChips={ | ||||
|                       subitem.attributes.metadata && | ||||
|                       subitem.attributes.metadata.length > 0 && | ||||
|                       subitem.attributes.metadata[0] | ||||
|                         ? [prettyItemSubType(subitem.attributes.metadata[0])] | ||||
|                         : [] | ||||
|                     } | ||||
|                     bottomChips={subitem.attributes.categories?.data.map( | ||||
|                       (category) => category.attributes?.short ?? "" | ||||
|                 {item.release_date && ( | ||||
|                   <div className="grid place-content-start place-items-center"> | ||||
|                     <h3 className="text-xl">{langui.release_date}</h3> | ||||
|                     <p>{prettyDate(item.release_date)}</p> | ||||
|                   </div> | ||||
|                 )} | ||||
| 
 | ||||
|                 {item.price && ( | ||||
|                   <div className="grid place-content-start place-items-center text-center"> | ||||
|                     <h3 className="text-xl">{langui.price}</h3> | ||||
|                     <p> | ||||
|                       {prettyPrice( | ||||
|                         item.price, | ||||
|                         currencies, | ||||
|                         item.price.currency?.data?.attributes?.code | ||||
|                       )} | ||||
|                     </p> | ||||
|                     {item.price.currency?.data?.attributes?.code !== | ||||
|                       appLayout.currency && ( | ||||
|                       <p> | ||||
|                         {prettyPrice( | ||||
|                           item.price, | ||||
|                           currencies, | ||||
|                           appLayout.currency | ||||
|                         )}{" "} | ||||
|                         <br />({langui.calculated?.toLowerCase()}) | ||||
|                       </p> | ||||
|                     )} | ||||
|                     metadata={{ | ||||
|                       currencies: currencies, | ||||
|                       release_date: subitem.attributes.release_date, | ||||
|                       price: subitem.attributes.price, | ||||
|                       position: "Bottom", | ||||
|                     }} | ||||
|                     infoAppend={ | ||||
|                       <PreviewCardCTAs | ||||
|                         id={subitem.id} | ||||
|                         langui={langui} | ||||
|                         displayCTAs={ | ||||
|                           !isUntangibleGroupItem( | ||||
|                             subitem.attributes.metadata?.[0] | ||||
|                           ) | ||||
|                         } | ||||
|                       /> | ||||
|                     } | ||||
|                   /> | ||||
|                 </Fragment> | ||||
|               ))} | ||||
|             </div> | ||||
|           </div> | ||||
|         )} | ||||
|                   </div> | ||||
|                 )} | ||||
|               </div> | ||||
| 
 | ||||
|         {item?.contents && item.contents.data.length > 0 && ( | ||||
|           <div id="contents" className="grid w-full place-items-center gap-8"> | ||||
|             <h2 className="-mb-6 text-2xl">{langui.contents}</h2> | ||||
|             {displayOpenScans && ( | ||||
|               <Button | ||||
|                 href={`/library/${item.slug}/scans`} | ||||
|                 text={langui.view_scans} | ||||
|               /> | ||||
|             )} | ||||
|             <div className="grid w-full gap-4"> | ||||
|               {item.contents.data.map((content) => ( | ||||
|                 <ContentLine | ||||
|                   langui={langui} | ||||
|                   content={content} | ||||
|                   parentSlug={item.slug} | ||||
|                   key={content.id} | ||||
|                 /> | ||||
|               ))} | ||||
|               {item.categories && item.categories.data.length > 0 && ( | ||||
|                 <div className="flex flex-col place-items-center gap-2"> | ||||
|                   <h3 className="text-xl">{langui.categories}</h3> | ||||
|                   <div className="flex flex-row flex-wrap place-content-center gap-2"> | ||||
|                     {item.categories.data.map((category) => ( | ||||
|                       <Chip key={category.id}>{category.attributes?.name}</Chip> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|               {item.size && ( | ||||
|                 <div className="grid gap-8 mobile:place-items-center"> | ||||
|                   <h3 className="text-xl">{langui.size}</h3> | ||||
|                   <div | ||||
|                     className="grid w-full grid-flow-col place-content-between thin:grid-flow-row | ||||
|                   thin:place-content-center thin:gap-8" | ||||
|                   > | ||||
|                     <div | ||||
|                       className="grid place-items-center gap-x-4 desktop:grid-flow-col  | ||||
|                     desktop:place-items-start" | ||||
|                     > | ||||
|                       <p className="font-bold">{langui.width}:</p> | ||||
|                       <div> | ||||
|                         <p>{item.size.width} mm</p> | ||||
|                         <p>{convertMmToInch(item.size.width)} in</p> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                     <div | ||||
|                       className="grid place-items-center gap-x-4 desktop:grid-flow-col | ||||
|                     desktop:place-items-start" | ||||
|                     > | ||||
|                       <p className="font-bold">{langui.height}:</p> | ||||
|                       <div> | ||||
|                         <p>{item.size.height} mm</p> | ||||
|                         <p>{convertMmToInch(item.size.height)} in</p> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                     {isDefined(item.size.thickness) && ( | ||||
|                       <div | ||||
|                         className="grid place-items-center gap-x-4 desktop:grid-flow-col | ||||
|                       desktop:place-items-start" | ||||
|                       > | ||||
|                         <p className="font-bold">{langui.thickness}:</p> | ||||
|                         <div> | ||||
|                           <p>{item.size.thickness} mm</p> | ||||
|                           <p>{convertMmToInch(item.size.thickness)} in</p> | ||||
|                         </div> | ||||
|                       </div> | ||||
|                     )} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|               {item.metadata?.[0]?.__typename !== "ComponentMetadataGroup" && | ||||
|                 item.metadata?.[0]?.__typename !== "ComponentMetadataOther" && ( | ||||
|                   <> | ||||
|                     <h3 className="text-xl">{langui.type_information}</h3> | ||||
|                     <div className="grid w-full grid-cols-2 place-content-between"> | ||||
|                       {item.metadata?.[0]?.__typename === | ||||
|                         "ComponentMetadataBooks" && ( | ||||
|                         <> | ||||
|                           <div className="flex flex-row place-content-start gap-4"> | ||||
|                             <p className="font-bold">{langui.pages}:</p> | ||||
|                             <p>{item.metadata[0].page_count}</p> | ||||
|                           </div> | ||||
| 
 | ||||
|                           <div className="flex flex-row place-content-start gap-4"> | ||||
|                             <p className="font-bold">{langui.binding}:</p> | ||||
|                             <p> | ||||
|                               {item.metadata[0].binding_type === | ||||
|                               Enum_Componentmetadatabooks_Binding_Type.Paperback | ||||
|                                 ? langui.paperback | ||||
|                                 : item.metadata[0].binding_type === | ||||
|                                   Enum_Componentmetadatabooks_Binding_Type.Hardcover | ||||
|                                 ? langui.hardcover | ||||
|                                 : ""} | ||||
|                             </p> | ||||
|                           </div> | ||||
| 
 | ||||
|                           <div className="flex flex-row place-content-start gap-4"> | ||||
|                             <p className="font-bold">{langui.page_order}:</p> | ||||
|                             <p> | ||||
|                               {item.metadata[0].page_order === | ||||
|                               Enum_Componentmetadatabooks_Page_Order.LeftToRight | ||||
|                                 ? langui.left_to_right | ||||
|                                 : langui.right_to_left} | ||||
|                             </p> | ||||
|                           </div> | ||||
| 
 | ||||
|                           <div className="flex flex-row place-content-start gap-4"> | ||||
|                             <p className="font-bold">{langui.languages}:</p> | ||||
|                             {item.metadata[0]?.languages?.data.map((lang) => ( | ||||
|                               <p key={lang.attributes?.code}> | ||||
|                                 {lang.attributes?.name} | ||||
|                               </p> | ||||
|                             ))} | ||||
|                           </div> | ||||
|                         </> | ||||
|                       )} | ||||
|                     </div> | ||||
|                   </> | ||||
|                 )} | ||||
|             </div> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|     </ContentPanel> | ||||
|           </InsetBox> | ||||
| 
 | ||||
|           {item.subitems && item.subitems.data.length > 0 && ( | ||||
|             <div | ||||
|               id={isVariantSet ? "variants" : "subitems"} | ||||
|               className="grid w-full place-items-center gap-8" | ||||
|             > | ||||
|               <h2 className="text-2xl"> | ||||
|                 {isVariantSet ? langui.variants : langui.subitems} | ||||
|               </h2> | ||||
| 
 | ||||
|               {hoverable && ( | ||||
|                 <WithLabel | ||||
|                   label={langui.always_show_info} | ||||
|                   input={ | ||||
|                     <Switch | ||||
|                       setState={setKeepInfoVisible} | ||||
|                       state={keepInfoVisible} | ||||
|                     /> | ||||
|                   } | ||||
|                 /> | ||||
|               )} | ||||
| 
 | ||||
|               <div | ||||
|                 className="grid w-full grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] | ||||
|               items-end gap-8 mobile:grid-cols-2 thin:grid-cols-1" | ||||
|               > | ||||
|                 {filterHasAttributes(item.subitems.data).map((subitem) => ( | ||||
|                   <Fragment key={subitem.id}> | ||||
|                     <PreviewCard | ||||
|                       href={`/library/${subitem.attributes.slug}`} | ||||
|                       title={subitem.attributes.title} | ||||
|                       subtitle={subitem.attributes.subtitle} | ||||
|                       thumbnail={subitem.attributes.thumbnail?.data?.attributes} | ||||
|                       thumbnailAspectRatio="21/29.7" | ||||
|                       thumbnailRounded={false} | ||||
|                       keepInfoVisible={keepInfoVisible} | ||||
|                       topChips={ | ||||
|                         subitem.attributes.metadata && | ||||
|                         subitem.attributes.metadata.length > 0 && | ||||
|                         subitem.attributes.metadata[0] | ||||
|                           ? [prettyItemSubType(subitem.attributes.metadata[0])] | ||||
|                           : [] | ||||
|                       } | ||||
|                       bottomChips={subitem.attributes.categories?.data.map( | ||||
|                         (category) => category.attributes?.short ?? "" | ||||
|                       )} | ||||
|                       metadata={{ | ||||
|                         currencies: currencies, | ||||
|                         release_date: subitem.attributes.release_date, | ||||
|                         price: subitem.attributes.price, | ||||
|                         position: "Bottom", | ||||
|                       }} | ||||
|                       infoAppend={ | ||||
|                         <PreviewCardCTAs | ||||
|                           id={subitem.id} | ||||
|                           langui={langui} | ||||
|                           displayCTAs={ | ||||
|                             !isUntangibleGroupItem( | ||||
|                               subitem.attributes.metadata?.[0] | ||||
|                             ) | ||||
|                           } | ||||
|                         /> | ||||
|                       } | ||||
|                     /> | ||||
|                   </Fragment> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
| 
 | ||||
|           {item.contents && item.contents.data.length > 0 && ( | ||||
|             <div id="contents" className="grid w-full place-items-center gap-8"> | ||||
|               <h2 className="-mb-6 text-2xl">{langui.contents}</h2> | ||||
|               {displayOpenScans && ( | ||||
|                 <Button | ||||
|                   href={`/library/${item.slug}/scans`} | ||||
|                   text={langui.view_scans} | ||||
|                 /> | ||||
|               )} | ||||
|               <div className="grid w-full gap-4"> | ||||
|                 {item.contents.data.map((content) => ( | ||||
|                   <ContentLine | ||||
|                     langui={langui} | ||||
|                     content={content} | ||||
|                     parentSlug={item.slug} | ||||
|                     key={content.id} | ||||
|                   /> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
|         </div> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       LightBox, | ||||
|       openLightBox, | ||||
|       appLayout.currency, | ||||
|       currencies, | ||||
|       displayOpenScans, | ||||
|       hoverable, | ||||
|       isVariantSet, | ||||
|       item, | ||||
|       itemId, | ||||
|       keepInfoVisible, | ||||
|       langui, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout | ||||
|       navTitle={prettyinlineTitle("", item?.title, item?.subtitle)} | ||||
|       navTitle={prettyinlineTitle("", item.title, item.subtitle)} | ||||
|       contentPanel={contentPanel} | ||||
|       subPanel={subPanel} | ||||
|       thumbnail={item?.thumbnail?.data?.attributes ?? undefined} | ||||
|       description={item?.descriptions?.[0]?.description ?? undefined} | ||||
|       thumbnail={item.thumbnail?.data?.attributes ?? undefined} | ||||
|       description={item.descriptions?.[0]?.description ?? undefined} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| @ -526,6 +554,7 @@ export async function getStaticProps( | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
|   if (!item.libraryItems?.data[0]?.attributes) return { notFound: true }; | ||||
|   sortContent(item.libraryItems.data[0].attributes.contents); | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     item: item.libraryItems.data[0].attributes, | ||||
| @ -544,7 +573,7 @@ export async function getStaticPaths( | ||||
|   const paths: GetStaticPathsResult["paths"] = []; | ||||
|   filterHasAttributes(libraryItems.libraryItems?.data).map((item) => { | ||||
|     context.locales?.map((local) => | ||||
|       paths.push({ params: { slug: item.attributes?.slug }, locale: local }) | ||||
|       paths.push({ params: { slug: item.attributes.slug }, locale: local }) | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|  | ||||
| @ -23,96 +23,112 @@ import { | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { Fragment } from "react"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   item: NonNullable< | ||||
|     GetLibraryItemScansQuery["libraryItems"] | ||||
|   >["data"][number]["attributes"]; | ||||
|     NonNullable< | ||||
|       GetLibraryItemScansQuery["libraryItems"] | ||||
|     >["data"][number]["attributes"] | ||||
|   >; | ||||
|   itemId: NonNullable< | ||||
|     GetLibraryItemScansQuery["libraryItems"] | ||||
|   >["data"][number]["id"]; | ||||
|     NonNullable<GetLibraryItemScansQuery["libraryItems"]>["data"][number]["id"] | ||||
|   >; | ||||
| } | ||||
| 
 | ||||
| export default function LibrarySlug(props: Props): JSX.Element { | ||||
|   const { item, langui, languages } = props; | ||||
|   const [openLightBox, LightBox] = useLightBox(); | ||||
|   sortContent(item?.contents); | ||||
|   sortContent(item.contents); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href={`/library/${item?.slug}`} | ||||
|         title={langui.item} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Desktop} | ||||
|         horizontalLine | ||||
|       /> | ||||
| 
 | ||||
|       {item?.contents?.data.map((content) => ( | ||||
|         <NavOption | ||||
|           key={content.id} | ||||
|           url={`#${content.attributes?.slug}`} | ||||
|           title={prettySlug(content.attributes?.slug, item.slug)} | ||||
|           subtitle={ | ||||
|             content.attributes?.range[0]?.__typename === | ||||
|             "ComponentRangePageRange" | ||||
|               ? `${content.attributes.range[0].starting_page}` + | ||||
|                 `→` + | ||||
|                 `${content.attributes.range[0].ending_page}` | ||||
|               : undefined | ||||
|           } | ||||
|           border | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <ReturnButton | ||||
|           href={`/library/${item.slug}`} | ||||
|           title={langui.item} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Desktop} | ||||
|           horizontalLine | ||||
|         /> | ||||
|       ))} | ||||
|     </SubPanel> | ||||
| 
 | ||||
|         {item.contents?.data.map((content) => ( | ||||
|           <NavOption | ||||
|             key={content.id} | ||||
|             url={`#${content.attributes?.slug}`} | ||||
|             title={prettySlug(content.attributes?.slug, item.slug)} | ||||
|             subtitle={ | ||||
|               content.attributes?.range[0]?.__typename === | ||||
|               "ComponentRangePageRange" | ||||
|                 ? `${content.attributes.range[0].starting_page}` + | ||||
|                   `→` + | ||||
|                   `${content.attributes.range[0].ending_page}` | ||||
|                 : undefined | ||||
|             } | ||||
|             border | ||||
|           /> | ||||
|         ))} | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [item.contents?.data, item.slug, langui] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       <LightBox /> | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         <LightBox /> | ||||
| 
 | ||||
|       <ReturnButton | ||||
|         href={`/library/${item?.slug}`} | ||||
|         title={langui.item} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Mobile} | ||||
|         className="mb-10" | ||||
|       /> | ||||
| 
 | ||||
|       {item?.images && ( | ||||
|         <ScanSetCover | ||||
|           images={item.images} | ||||
|           openLightBox={openLightBox} | ||||
|           languages={languages} | ||||
|         <ReturnButton | ||||
|           href={`/library/${item.slug}`} | ||||
|           title={langui.item} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Mobile} | ||||
|           className="mb-10" | ||||
|         /> | ||||
|       )} | ||||
| 
 | ||||
|       {item?.contents?.data.map((content) => ( | ||||
|         <Fragment key={content.id}> | ||||
|           {content.attributes?.scan_set?.[0] && ( | ||||
|             <ScanSet | ||||
|               scanSet={content.attributes.scan_set} | ||||
|               openLightBox={openLightBox} | ||||
|               slug={content.attributes.slug} | ||||
|               title={prettySlug(content.attributes.slug, item.slug)} | ||||
|               languages={languages} | ||||
|               langui={langui} | ||||
|               content={content.attributes.content} | ||||
|             /> | ||||
|           )} | ||||
|         </Fragment> | ||||
|       ))} | ||||
|     </ContentPanel> | ||||
|         {item.images && ( | ||||
|           <ScanSetCover | ||||
|             images={item.images} | ||||
|             openLightBox={openLightBox} | ||||
|             languages={languages} | ||||
|             langui={langui} | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
|         {item.contents?.data.map((content) => ( | ||||
|           <Fragment key={content.id}> | ||||
|             {content.attributes?.scan_set?.[0] && ( | ||||
|               <ScanSet | ||||
|                 scanSet={content.attributes.scan_set} | ||||
|                 openLightBox={openLightBox} | ||||
|                 slug={content.attributes.slug} | ||||
|                 title={prettySlug(content.attributes.slug, item.slug)} | ||||
|                 languages={languages} | ||||
|                 langui={langui} | ||||
|                 content={content.attributes.content} | ||||
|               /> | ||||
|             )} | ||||
|           </Fragment> | ||||
|         ))} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       LightBox, | ||||
|       openLightBox, | ||||
|       item.contents?.data, | ||||
|       item.images, | ||||
|       item.slug, | ||||
|       languages, | ||||
|       langui, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout | ||||
|       navTitle={prettyinlineTitle("", item?.title, item?.subtitle)} | ||||
|       navTitle={prettyinlineTitle("", item.title, item.subtitle)} | ||||
|       contentPanel={contentPanel} | ||||
|       subPanel={subPanel} | ||||
|       thumbnail={item?.thumbnail?.data?.attributes ?? undefined} | ||||
|       thumbnail={item.thumbnail?.data?.attributes ?? undefined} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| @ -129,7 +145,8 @@ export async function getStaticProps( | ||||
|         : "", | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
|   if (!item.libraryItems?.data[0]?.attributes) return { notFound: true }; | ||||
|   if (!item.libraryItems?.data[0]?.attributes || !item.libraryItems.data[0]?.id) | ||||
|     return { notFound: true }; | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     item: item.libraryItems.data[0].attributes, | ||||
|  | ||||
| @ -114,204 +114,226 @@ export default function Library(props: Props): JSX.Element { | ||||
|     [langui, groupingMethod, sortedItems] | ||||
|   ); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
|         icon={Icon.LibraryBooks} | ||||
|         title={langui.library} | ||||
|         description={langui.library_description} | ||||
|       /> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <PanelHeader | ||||
|           icon={Icon.LibraryBooks} | ||||
|           title={langui.library} | ||||
|           description={langui.library_description} | ||||
|         /> | ||||
| 
 | ||||
|       <TextInput | ||||
|         className="mb-6 w-full" | ||||
|         placeholder={langui.search_title ?? undefined} | ||||
|         state={searchName} | ||||
|         setState={setSearchName} | ||||
|       /> | ||||
|         <TextInput | ||||
|           className="mb-6 w-full" | ||||
|           placeholder={langui.search_title ?? undefined} | ||||
|           state={searchName} | ||||
|           setState={setSearchName} | ||||
|         /> | ||||
| 
 | ||||
|       <WithLabel | ||||
|         label={langui.group_by} | ||||
|         input={ | ||||
|           <Select | ||||
|             className="w-full" | ||||
|             options={[ | ||||
|               langui.category ?? "Category", | ||||
|               langui.type ?? "Type", | ||||
|               langui.release_year ?? "Year", | ||||
|             ]} | ||||
|             state={groupingMethod} | ||||
|             setState={setGroupingMethod} | ||||
|             allowEmpty | ||||
|           /> | ||||
|         } | ||||
|       /> | ||||
| 
 | ||||
|       <WithLabel | ||||
|         label={langui.order_by} | ||||
|         input={ | ||||
|           <Select | ||||
|             className="w-full" | ||||
|             options={[ | ||||
|               langui.name ?? "Name", | ||||
|               langui.price ?? "Price", | ||||
|               langui.release_date ?? "Release date", | ||||
|             ]} | ||||
|             state={sortingMethod} | ||||
|             setState={setSortingMethod} | ||||
|           /> | ||||
|         } | ||||
|       /> | ||||
| 
 | ||||
|       <WithLabel | ||||
|         label={langui.show_subitems} | ||||
|         input={<Switch state={showSubitems} setState={setShowSubitems} />} | ||||
|       /> | ||||
| 
 | ||||
|       <WithLabel | ||||
|         label={langui.show_primary_items} | ||||
|         input={ | ||||
|           <Switch state={showPrimaryItems} setState={setShowPrimaryItems} /> | ||||
|         } | ||||
|       /> | ||||
| 
 | ||||
|       <WithLabel | ||||
|         label={langui.show_secondary_items} | ||||
|         input={ | ||||
|           <Switch state={showSecondaryItems} setState={setShowSecondaryItems} /> | ||||
|         } | ||||
|       /> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel | ||||
|           label={langui.always_show_info} | ||||
|           label={langui.group_by} | ||||
|           input={ | ||||
|             <Switch state={keepInfoVisible} setState={setKeepInfoVisible} /> | ||||
|             <Select | ||||
|               className="w-full" | ||||
|               options={[ | ||||
|                 langui.category ?? "Category", | ||||
|                 langui.type ?? "Type", | ||||
|                 langui.release_year ?? "Year", | ||||
|               ]} | ||||
|               state={groupingMethod} | ||||
|               setState={setGroupingMethod} | ||||
|               allowEmpty | ||||
|             /> | ||||
|           } | ||||
|         /> | ||||
|       )} | ||||
| 
 | ||||
|       <ButtonGroup className="mt-4"> | ||||
|         <ToolTip content={langui.only_display_items_i_want}> | ||||
|           <Button | ||||
|             icon={Icon.Favorite} | ||||
|             onClick={() => setFilterUserStatus(LibraryItemUserStatus.Want)} | ||||
|             active={filterUserStatus === LibraryItemUserStatus.Want} | ||||
|           /> | ||||
|         </ToolTip> | ||||
|         <ToolTip content={langui.only_display_items_i_have}> | ||||
|           <Button | ||||
|             icon={Icon.BackHand} | ||||
|             onClick={() => setFilterUserStatus(LibraryItemUserStatus.Have)} | ||||
|             active={filterUserStatus === LibraryItemUserStatus.Have} | ||||
|           /> | ||||
|         </ToolTip> | ||||
|         <ToolTip content={langui.only_display_unmarked_items}> | ||||
|           <Button | ||||
|             icon={Icon.RadioButtonUnchecked} | ||||
|             onClick={() => setFilterUserStatus(LibraryItemUserStatus.None)} | ||||
|             active={filterUserStatus === LibraryItemUserStatus.None} | ||||
|           /> | ||||
|         </ToolTip> | ||||
|         <ToolTip content={langui.display_all_items}> | ||||
|           <Button | ||||
|             text={"All"} | ||||
|             onClick={() => setFilterUserStatus(undefined)} | ||||
|             active={isUndefined(filterUserStatus)} | ||||
|           /> | ||||
|         </ToolTip> | ||||
|       </ButtonGroup> | ||||
|         <WithLabel | ||||
|           label={langui.order_by} | ||||
|           input={ | ||||
|             <Select | ||||
|               className="w-full" | ||||
|               options={[ | ||||
|                 langui.name ?? "Name", | ||||
|                 langui.price ?? "Price", | ||||
|                 langui.release_date ?? "Release date", | ||||
|               ]} | ||||
|               state={sortingMethod} | ||||
|               setState={setSortingMethod} | ||||
|             /> | ||||
|           } | ||||
|         /> | ||||
| 
 | ||||
|       <Button | ||||
|         className="mt-8" | ||||
|         text={langui.reset_all_filters} | ||||
|         icon={Icon.Replay} | ||||
|         onClick={() => { | ||||
|           setSearchName(defaultFiltersState.searchName); | ||||
|           setShowSubitems(defaultFiltersState.showSubitems); | ||||
|           setShowPrimaryItems(defaultFiltersState.showPrimaryItems); | ||||
|           setShowSecondaryItems(defaultFiltersState.showSecondaryItems); | ||||
|           setSortingMethod(defaultFiltersState.sortingMethod); | ||||
|           setGroupingMethod(defaultFiltersState.groupingMethod); | ||||
|           setKeepInfoVisible(defaultFiltersState.keepInfoVisible); | ||||
|           setFilterUserStatus(defaultFiltersState.filterUserStatus); | ||||
|         }} | ||||
|       /> | ||||
|     </SubPanel> | ||||
|         <WithLabel | ||||
|           label={langui.show_subitems} | ||||
|           input={<Switch state={showSubitems} setState={setShowSubitems} />} | ||||
|         /> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.show_primary_items} | ||||
|           input={ | ||||
|             <Switch state={showPrimaryItems} setState={setShowPrimaryItems} /> | ||||
|           } | ||||
|         /> | ||||
| 
 | ||||
|         <WithLabel | ||||
|           label={langui.show_secondary_items} | ||||
|           input={ | ||||
|             <Switch | ||||
|               state={showSecondaryItems} | ||||
|               setState={setShowSecondaryItems} | ||||
|             /> | ||||
|           } | ||||
|         /> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|               <Switch state={keepInfoVisible} setState={setKeepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
|         <ButtonGroup className="mt-4"> | ||||
|           <ToolTip content={langui.only_display_items_i_want}> | ||||
|             <Button | ||||
|               icon={Icon.Favorite} | ||||
|               onClick={() => setFilterUserStatus(LibraryItemUserStatus.Want)} | ||||
|               active={filterUserStatus === LibraryItemUserStatus.Want} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|           <ToolTip content={langui.only_display_items_i_have}> | ||||
|             <Button | ||||
|               icon={Icon.BackHand} | ||||
|               onClick={() => setFilterUserStatus(LibraryItemUserStatus.Have)} | ||||
|               active={filterUserStatus === LibraryItemUserStatus.Have} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|           <ToolTip content={langui.only_display_unmarked_items}> | ||||
|             <Button | ||||
|               icon={Icon.RadioButtonUnchecked} | ||||
|               onClick={() => setFilterUserStatus(LibraryItemUserStatus.None)} | ||||
|               active={filterUserStatus === LibraryItemUserStatus.None} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|           <ToolTip content={langui.display_all_items}> | ||||
|             <Button | ||||
|               text={"All"} | ||||
|               onClick={() => setFilterUserStatus(undefined)} | ||||
|               active={isUndefined(filterUserStatus)} | ||||
|             /> | ||||
|           </ToolTip> | ||||
|         </ButtonGroup> | ||||
| 
 | ||||
|         <Button | ||||
|           className="mt-8" | ||||
|           text={langui.reset_all_filters} | ||||
|           icon={Icon.Replay} | ||||
|           onClick={() => { | ||||
|             setSearchName(defaultFiltersState.searchName); | ||||
|             setShowSubitems(defaultFiltersState.showSubitems); | ||||
|             setShowPrimaryItems(defaultFiltersState.showPrimaryItems); | ||||
|             setShowSecondaryItems(defaultFiltersState.showSecondaryItems); | ||||
|             setSortingMethod(defaultFiltersState.sortingMethod); | ||||
|             setGroupingMethod(defaultFiltersState.groupingMethod); | ||||
|             setKeepInfoVisible(defaultFiltersState.keepInfoVisible); | ||||
|             setFilterUserStatus(defaultFiltersState.filterUserStatus); | ||||
|           }} | ||||
|         /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [ | ||||
|       filterUserStatus, | ||||
|       groupingMethod, | ||||
|       hoverable, | ||||
|       keepInfoVisible, | ||||
|       langui, | ||||
|       searchName, | ||||
|       showPrimaryItems, | ||||
|       showSecondaryItems, | ||||
|       showSubitems, | ||||
|       sortingMethod, | ||||
|     ] | ||||
|   ); | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       {/* TODO: Add to langui */} | ||||
|       {groups.size === 0 && ( | ||||
|         <ContentPlaceholder | ||||
|           message={ | ||||
|             "No results. You can try changing or resetting the search parameters." | ||||
|           } | ||||
|           icon={Icon.ChevronLeft} | ||||
|         /> | ||||
|       )} | ||||
|       {iterateMap(groups, (name, items) => ( | ||||
|         <Fragment key={name}> | ||||
|           {isDefinedAndNotEmpty(name) && ( | ||||
|             <h2 | ||||
|               className="flex flex-row place-items-center gap-2 | ||||
| 
 | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         {/* TODO: Add to langui */} | ||||
|         {groups.size === 0 && ( | ||||
|           <ContentPlaceholder | ||||
|             message={ | ||||
|               "No results. You can try changing or resetting the search parameters." | ||||
|             } | ||||
|             icon={Icon.ChevronLeft} | ||||
|           /> | ||||
|         )} | ||||
|         {iterateMap(groups, (name, items) => ( | ||||
|           <Fragment key={name}> | ||||
|             {isDefinedAndNotEmpty(name) && ( | ||||
|               <h2 | ||||
|                 className="flex flex-row place-items-center gap-2 | ||||
|                   pb-2 pt-10 text-2xl first-of-type:pt-0" | ||||
|             > | ||||
|               {name} | ||||
|               <Chip>{`${items.length} ${ | ||||
|                 items.length <= 1 | ||||
|                   ? langui.result?.toLowerCase() ?? "result" | ||||
|                   : langui.results?.toLowerCase() ?? "results" | ||||
|               }`}</Chip>
 | ||||
|             </h2> | ||||
|           )} | ||||
|           <div | ||||
|             className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 | ||||
|               > | ||||
|                 {name} | ||||
|                 <Chip>{`${items.length} ${ | ||||
|                   items.length <= 1 | ||||
|                     ? langui.result?.toLowerCase() ?? "result" | ||||
|                     : langui.results?.toLowerCase() ?? "results" | ||||
|                 }`}</Chip>
 | ||||
|               </h2> | ||||
|             )} | ||||
|             <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(13rem,1fr))] | ||||
|                 mobile:grid-cols-2 mobile:gap-4" | ||||
|           > | ||||
|             {filterHasAttributes(items).map((item) => ( | ||||
|               <Fragment key={item.id}> | ||||
|                 <PreviewCard | ||||
|                   href={`/library/${item.attributes.slug}`} | ||||
|                   title={item.attributes.title} | ||||
|                   subtitle={item.attributes.subtitle} | ||||
|                   thumbnail={item.attributes.thumbnail?.data?.attributes} | ||||
|                   thumbnailAspectRatio="21/29.7" | ||||
|                   thumbnailRounded={false} | ||||
|                   keepInfoVisible={keepInfoVisible} | ||||
|                   topChips={ | ||||
|                     item.attributes.metadata && | ||||
|                     item.attributes.metadata.length > 0 && | ||||
|                     item.attributes.metadata[0] | ||||
|                       ? [prettyItemSubType(item.attributes.metadata[0])] | ||||
|                       : [] | ||||
|                   } | ||||
|                   bottomChips={item.attributes.categories?.data.map( | ||||
|                     (category) => category.attributes?.short ?? "" | ||||
|                   )} | ||||
|                   metadata={{ | ||||
|                     currencies: currencies, | ||||
|                     release_date: item.attributes.release_date, | ||||
|                     price: item.attributes.price, | ||||
|                     position: "Bottom", | ||||
|                   }} | ||||
|                   infoAppend={ | ||||
|                     <PreviewCardCTAs | ||||
|                       id={item.id} | ||||
|                       displayCTAs={ | ||||
|                         !isUntangibleGroupItem(item.attributes.metadata?.[0]) | ||||
|                       } | ||||
|                       langui={langui} | ||||
|                     /> | ||||
|                   } | ||||
|                 /> | ||||
|               </Fragment> | ||||
|             ))} | ||||
|           </div> | ||||
|         </Fragment> | ||||
|       ))} | ||||
|     </ContentPanel> | ||||
|             > | ||||
|               {filterHasAttributes(items).map((item) => ( | ||||
|                 <Fragment key={item.id}> | ||||
|                   <PreviewCard | ||||
|                     href={`/library/${item.attributes.slug}`} | ||||
|                     title={item.attributes.title} | ||||
|                     subtitle={item.attributes.subtitle} | ||||
|                     thumbnail={item.attributes.thumbnail?.data?.attributes} | ||||
|                     thumbnailAspectRatio="21/29.7" | ||||
|                     thumbnailRounded={false} | ||||
|                     keepInfoVisible={keepInfoVisible} | ||||
|                     topChips={ | ||||
|                       item.attributes.metadata && | ||||
|                       item.attributes.metadata.length > 0 && | ||||
|                       item.attributes.metadata[0] | ||||
|                         ? [prettyItemSubType(item.attributes.metadata[0])] | ||||
|                         : [] | ||||
|                     } | ||||
|                     bottomChips={item.attributes.categories?.data.map( | ||||
|                       (category) => category.attributes?.short ?? "" | ||||
|                     )} | ||||
|                     metadata={{ | ||||
|                       currencies: currencies, | ||||
|                       release_date: item.attributes.release_date, | ||||
|                       price: item.attributes.price, | ||||
|                       position: "Bottom", | ||||
|                     }} | ||||
|                     infoAppend={ | ||||
|                       <PreviewCardCTAs | ||||
|                         id={item.id} | ||||
|                         displayCTAs={ | ||||
|                           !isUntangibleGroupItem(item.attributes.metadata?.[0]) | ||||
|                         } | ||||
|                         langui={langui} | ||||
|                       /> | ||||
|                     } | ||||
|                   /> | ||||
|                 </Fragment> | ||||
|               ))} | ||||
|             </div> | ||||
|           </Fragment> | ||||
|         ))} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [currencies, groups, keepInfoVisible, langui] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppLayout | ||||
|       navTitle={langui.library} | ||||
|  | ||||
| @ -5,20 +5,24 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| 
 | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { Icon } from "components/Ico"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| export default function Merch(props: Props): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
|         icon={Icon.Store} | ||||
|         title={langui.merch} | ||||
|         description={langui.merch_description} | ||||
|       /> | ||||
|     </SubPanel> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <PanelHeader | ||||
|           icon={Icon.Store} | ||||
|           title={langui.merch} | ||||
|           description={langui.merch_description} | ||||
|         /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [langui] | ||||
|   ); | ||||
| 
 | ||||
|    | ||||
|   return <AppLayout navTitle={langui.merch} subPanel={subPanel} {...props} />; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -46,73 +46,79 @@ export default function News(props: Props): JSX.Element { | ||||
|     [posts, searchName] | ||||
|   ); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
|         icon={Icon.Feed} | ||||
|         title={langui.news} | ||||
|         description={langui.news_description} | ||||
|       /> | ||||
| 
 | ||||
|       <TextInput | ||||
|         className="mb-6 w-full" | ||||
|         placeholder={langui.search_title ?? undefined} | ||||
|         state={searchName} | ||||
|         setState={setSearchName} | ||||
|       /> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel | ||||
|           label={langui.always_show_info} | ||||
|           input={ | ||||
|             <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|           } | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <PanelHeader | ||||
|           icon={Icon.Feed} | ||||
|           title={langui.news} | ||||
|           description={langui.news_description} | ||||
|         /> | ||||
|       )} | ||||
| 
 | ||||
|       <Button | ||||
|         className="mt-8" | ||||
|         text={langui.reset_all_filters} | ||||
|         icon={Icon.Replay} | ||||
|         onClick={() => { | ||||
|           setSearchName(defaultFiltersState.searchName); | ||||
|           setKeepInfoVisible(defaultFiltersState.keepInfoVisible); | ||||
|         }} | ||||
|       /> | ||||
|     </SubPanel> | ||||
|         <TextInput | ||||
|           className="mb-6 w-full" | ||||
|           placeholder={langui.search_title ?? undefined} | ||||
|           state={searchName} | ||||
|           setState={setSearchName} | ||||
|         /> | ||||
| 
 | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|               <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|             } | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
|         <Button | ||||
|           className="mt-8" | ||||
|           text={langui.reset_all_filters} | ||||
|           icon={Icon.Replay} | ||||
|           onClick={() => { | ||||
|             setSearchName(defaultFiltersState.searchName); | ||||
|             setKeepInfoVisible(defaultFiltersState.keepInfoVisible); | ||||
|           }} | ||||
|         /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [hoverable, keepInfoVisible, langui, searchName] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       <div | ||||
|         className="grid grid-cols-1 items-end gap-8 | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         <div | ||||
|           className="grid grid-cols-1 items-end gap-8 | ||||
|         desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]" | ||||
|       > | ||||
|         {filterHasAttributes(filteredItems).map((post) => ( | ||||
|           <Fragment key={post.id}> | ||||
|             <PreviewCard | ||||
|               href={`/news/${post.attributes.slug}`} | ||||
|               title={ | ||||
|                 post.attributes.translations?.[0]?.title ?? | ||||
|                 prettySlug(post.attributes.slug) | ||||
|               } | ||||
|               description={post.attributes.translations?.[0]?.excerpt} | ||||
|               thumbnail={post.attributes.thumbnail?.data?.attributes} | ||||
|               thumbnailAspectRatio="3/2" | ||||
|               thumbnailForceAspectRatio | ||||
|               bottomChips={post.attributes.categories?.data.map( | ||||
|                 (category) => category.attributes?.short ?? "" | ||||
|               )} | ||||
|               keepInfoVisible={keepInfoVisible} | ||||
|               metadata={{ | ||||
|                 release_date: post.attributes.date, | ||||
|                 position: "Top", | ||||
|               }} | ||||
|             /> | ||||
|           </Fragment> | ||||
|         ))} | ||||
|       </div> | ||||
|     </ContentPanel> | ||||
|         > | ||||
|           {filterHasAttributes(filteredItems).map((post) => ( | ||||
|             <Fragment key={post.id}> | ||||
|               <PreviewCard | ||||
|                 href={`/news/${post.attributes.slug}`} | ||||
|                 title={ | ||||
|                   post.attributes.translations?.[0]?.title ?? | ||||
|                   prettySlug(post.attributes.slug) | ||||
|                 } | ||||
|                 description={post.attributes.translations?.[0]?.excerpt} | ||||
|                 thumbnail={post.attributes.thumbnail?.data?.attributes} | ||||
|                 thumbnailAspectRatio="3/2" | ||||
|                 thumbnailForceAspectRatio | ||||
|                 bottomChips={post.attributes.categories?.data.map( | ||||
|                   (category) => category.attributes?.short ?? "" | ||||
|                 )} | ||||
|                 keepInfoVisible={keepInfoVisible} | ||||
|                 metadata={{ | ||||
|                   release_date: post.attributes.date, | ||||
|                   position: "Top", | ||||
|                 }} | ||||
|               /> | ||||
|             </Fragment> | ||||
|           ))} | ||||
|         </div> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [filteredItems, keepInfoVisible] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -26,6 +26,7 @@ import { | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   page: WikiPageWithTranslations; | ||||
| @ -40,80 +41,95 @@ export default function WikiPage(props: Props): JSX.Element { | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|   }); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href={`/wiki`} | ||||
|         title={langui.wiki} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Desktop} | ||||
|         horizontalLine | ||||
|       /> | ||||
|     </SubPanel> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <ReturnButton | ||||
|           href={`/wiki`} | ||||
|           title={langui.wiki} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Desktop} | ||||
|           horizontalLine | ||||
|         /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [langui] | ||||
|   ); | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Large}> | ||||
|       <ReturnButton | ||||
|         href={`/wiki`} | ||||
|         title={langui.wiki} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Mobile} | ||||
|         className="mb-10" | ||||
|       /> | ||||
| 
 | ||||
|       <div className="flex place-content-center gap-4"> | ||||
|         <h1 className="text-center text-3xl">{selectedTranslation?.title}</h1> | ||||
|         <LanguageSwitcher /> | ||||
|       </div> | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Large}> | ||||
|         <ReturnButton | ||||
|           href={`/wiki`} | ||||
|           title={langui.wiki} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Mobile} | ||||
|           className="mb-10" | ||||
|         /> | ||||
| 
 | ||||
|       <HorizontalLine /> | ||||
|         <div className="flex place-content-center gap-4"> | ||||
|           <h1 className="text-center text-3xl">{selectedTranslation?.title}</h1> | ||||
|           <LanguageSwitcher /> | ||||
|         </div> | ||||
| 
 | ||||
|       {selectedTranslation && ( | ||||
|         <div className="text-justify"> | ||||
|           <div | ||||
|             className="float-right ml-8 mb-8 w-[25rem] overflow-hidden rounded-lg bg-mid | ||||
|         <HorizontalLine /> | ||||
| 
 | ||||
|         {selectedTranslation && ( | ||||
|           <div className="text-justify"> | ||||
|             <div | ||||
|               className="float-right ml-8 mb-8 w-[25rem] overflow-hidden rounded-lg bg-mid | ||||
|             text-center" | ||||
|           > | ||||
|             {page.thumbnail?.data?.attributes && ( | ||||
|               <Img image={page.thumbnail.data.attributes} /> | ||||
|             )} | ||||
|             <div className="my-4 grid gap-4 p-4"> | ||||
|               <p className="font-headers text-xl">{langui.categories}</p> | ||||
|               <div className="flex flex-row flex-wrap place-content-center gap-2"> | ||||
|                 {page.categories?.data.map((category) => ( | ||||
|                   <Chip key={category.id}>{category.attributes?.name}</Chip> | ||||
|                 ))} | ||||
|             > | ||||
|               {page.thumbnail?.data?.attributes && ( | ||||
|                 <Img image={page.thumbnail.data.attributes} /> | ||||
|               )} | ||||
|               <div className="my-4 grid gap-4 p-4"> | ||||
|                 <p className="font-headers text-xl">{langui.categories}</p> | ||||
|                 <div className="flex flex-row flex-wrap place-content-center gap-2"> | ||||
|                   {page.categories?.data.map((category) => ( | ||||
|                     <Chip key={category.id}>{category.attributes?.name}</Chip> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           {isDefinedAndNotEmpty(selectedTranslation.summary) && ( | ||||
|             <div className="mb-6"> | ||||
|               <p className="font-headers text-lg">{langui.summary}</p> | ||||
|               <p>{selectedTranslation.summary}</p> | ||||
|             </div> | ||||
|           )} | ||||
|             {isDefinedAndNotEmpty(selectedTranslation.summary) && ( | ||||
|               <div className="mb-6"> | ||||
|                 <p className="font-headers text-lg">{langui.summary}</p> | ||||
|                 <p>{selectedTranslation.summary}</p> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|           {filterHasAttributes(page.definitions, ["translations"]).map( | ||||
|             (definition, index) => ( | ||||
|               <DefinitionCard | ||||
|                 key={index} | ||||
|                 source={definition.source?.data?.attributes?.name} | ||||
|                 translations={filterHasAttributes(definition.translations).map( | ||||
|                   (translation) => ({ | ||||
|             {filterHasAttributes(page.definitions, ["translations"]).map( | ||||
|               (definition, index) => ( | ||||
|                 <DefinitionCard | ||||
|                   key={index} | ||||
|                   source={definition.source?.data?.attributes?.name} | ||||
|                   translations={filterHasAttributes( | ||||
|                     definition.translations | ||||
|                   ).map((translation) => ({ | ||||
|                     language: translation.language.data?.attributes?.code, | ||||
|                     definition: translation.definition, | ||||
|                     status: translation.status, | ||||
|                   }) | ||||
|                 )} | ||||
|                 index={index + 1} | ||||
|                 languages={languages} | ||||
|                 langui={langui} | ||||
|               /> | ||||
|             ) | ||||
|           )} | ||||
|         </div> | ||||
|       )} | ||||
|     </ContentPanel> | ||||
|                   }))} | ||||
|                   index={index + 1} | ||||
|                   languages={languages} | ||||
|                   langui={langui} | ||||
|                 /> | ||||
|               ) | ||||
|             )} | ||||
|           </div> | ||||
|         )} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [ | ||||
|       LanguageSwitcher, | ||||
|       languages, | ||||
|       langui, | ||||
|       page.categories?.data, | ||||
|       page.definitions, | ||||
|       page.thumbnail?.data?.attributes, | ||||
|       selectedTranslation, | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -14,7 +14,7 @@ import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { filterHasAttributes, isDefined } from "helpers/others"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { Fragment } from "react"; | ||||
| import { Fragment, useMemo } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   chronologyItems: NonNullable< | ||||
| @ -27,109 +27,113 @@ export default function Chronology(props: Props): JSX.Element { | ||||
|   const { chronologyItems, chronologyEras, langui } = props; | ||||
| 
 | ||||
|   // Group by year the Chronology items
 | ||||
|   const chronologyItemYearGroups: Props["chronologyItems"][number][][][] = []; | ||||
|   const chronologyItemYearGroups = useMemo(() => { | ||||
|     const memo: Props["chronologyItems"][number][][][] = []; | ||||
|     chronologyEras.map(() => { | ||||
|       memo.push([]); | ||||
|     }); | ||||
| 
 | ||||
|   chronologyEras.map(() => { | ||||
|     chronologyItemYearGroups.push([]); | ||||
|   }); | ||||
| 
 | ||||
|   let currentChronologyEraIndex = 0; | ||||
|   chronologyItems.map((item) => { | ||||
|     if (item.attributes) { | ||||
|       if ( | ||||
|         item.attributes.year > | ||||
|         (chronologyEras[currentChronologyEraIndex].attributes?.ending_year ?? | ||||
|           999999) | ||||
|       ) { | ||||
|         currentChronologyEraIndex += 1; | ||||
|     let currentChronologyEraIndex = 0; | ||||
|     chronologyItems.map((item) => { | ||||
|       if (item.attributes) { | ||||
|         if ( | ||||
|           item.attributes.year > | ||||
|           (chronologyEras[currentChronologyEraIndex].attributes?.ending_year ?? | ||||
|             999999) | ||||
|         ) { | ||||
|           currentChronologyEraIndex += 1; | ||||
|         } | ||||
|         if ( | ||||
|           Object.prototype.hasOwnProperty.call( | ||||
|             memo[currentChronologyEraIndex], | ||||
|             item.attributes.year | ||||
|           ) | ||||
|         ) { | ||||
|           memo[currentChronologyEraIndex][item.attributes.year].push(item); | ||||
|         } else { | ||||
|           memo[currentChronologyEraIndex][item.attributes.year] = [item]; | ||||
|         } | ||||
|       } | ||||
|       if ( | ||||
|         Object.prototype.hasOwnProperty.call( | ||||
|           chronologyItemYearGroups[currentChronologyEraIndex], | ||||
|           item.attributes.year | ||||
|         ) | ||||
|       ) { | ||||
|         chronologyItemYearGroups[currentChronologyEraIndex][ | ||||
|           item.attributes.year | ||||
|         ].push(item); | ||||
|       } else { | ||||
|         chronologyItemYearGroups[currentChronologyEraIndex][ | ||||
|           item.attributes.year | ||||
|         ] = [item]; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|     }); | ||||
|     return memo; | ||||
|   }, [chronologyEras, chronologyItems]); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href="/wiki" | ||||
|         title={langui.wiki} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Desktop} | ||||
|         horizontalLine | ||||
|       /> | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <ReturnButton | ||||
|           href="/wiki" | ||||
|           title={langui.wiki} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Desktop} | ||||
|           horizontalLine | ||||
|         /> | ||||
| 
 | ||||
|       {filterHasAttributes(chronologyEras).map((era) => ( | ||||
|         <Fragment key={era.id}> | ||||
|           <NavOption | ||||
|             url={`#${era.attributes.slug}`} | ||||
|             title={ | ||||
|               era.attributes.title && | ||||
|               era.attributes.title.length > 0 && | ||||
|               era.attributes.title[0] | ||||
|                 ? era.attributes.title[0].title | ||||
|                 : prettySlug(era.attributes.slug) | ||||
|             } | ||||
|             subtitle={`${era.attributes.starting_year} → ${era.attributes.ending_year}`} | ||||
|             border | ||||
|           /> | ||||
|         </Fragment> | ||||
|       ))} | ||||
|     </SubPanel> | ||||
|         {filterHasAttributes(chronologyEras).map((era) => ( | ||||
|           <Fragment key={era.id}> | ||||
|             <NavOption | ||||
|               url={`#${era.attributes.slug}`} | ||||
|               title={ | ||||
|                 era.attributes.title && | ||||
|                 era.attributes.title.length > 0 && | ||||
|                 era.attributes.title[0] | ||||
|                   ? era.attributes.title[0].title | ||||
|                   : prettySlug(era.attributes.slug) | ||||
|               } | ||||
|               subtitle={`${era.attributes.starting_year} → ${era.attributes.ending_year}`} | ||||
|               border | ||||
|             /> | ||||
|           </Fragment> | ||||
|         ))} | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [chronologyEras, langui] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
|       <ReturnButton | ||||
|         href="/wiki" | ||||
|         title={langui.wiki} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.Mobile} | ||||
|         className="mb-10" | ||||
|       /> | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel> | ||||
|         <ReturnButton | ||||
|           href="/wiki" | ||||
|           title={langui.wiki} | ||||
|           langui={langui} | ||||
|           displayOn={ReturnButtonType.Mobile} | ||||
|           className="mb-10" | ||||
|         /> | ||||
| 
 | ||||
|       {chronologyItemYearGroups.map((era, eraIndex) => ( | ||||
|         <Fragment key={eraIndex}> | ||||
|           <InsetBox | ||||
|             id={chronologyEras[eraIndex].attributes?.slug} | ||||
|             className="my-8 grid gap-4 text-center" | ||||
|           > | ||||
|             <h2 className="text-2xl"> | ||||
|               {chronologyEras[eraIndex].attributes?.title?.[0] | ||||
|                 ? chronologyEras[eraIndex].attributes?.title?.[0]?.title | ||||
|                 : prettySlug(chronologyEras[eraIndex].attributes?.slug)} | ||||
|             </h2> | ||||
|             <p className="whitespace-pre-line "> | ||||
|               {chronologyEras[eraIndex].attributes?.title?.[0] | ||||
|                 ? chronologyEras[eraIndex].attributes?.title?.[0]?.description | ||||
|                 : ""} | ||||
|             </p> | ||||
|           </InsetBox> | ||||
|           {era.map((items, index) => ( | ||||
|             <Fragment key={index}> | ||||
|               {items[0].attributes && isDefined(items[0].attributes.year) && ( | ||||
|                 <ChronologyYearComponent | ||||
|                   year={items[0].attributes.year} | ||||
|                   items={items} | ||||
|                   langui={langui} | ||||
|                 /> | ||||
|               )} | ||||
|             </Fragment> | ||||
|           ))} | ||||
|         </Fragment> | ||||
|       ))} | ||||
|     </ContentPanel> | ||||
|         {chronologyItemYearGroups.map((era, eraIndex) => ( | ||||
|           <Fragment key={eraIndex}> | ||||
|             <InsetBox | ||||
|               id={chronologyEras[eraIndex].attributes?.slug} | ||||
|               className="my-8 grid gap-4 text-center" | ||||
|             > | ||||
|               <h2 className="text-2xl"> | ||||
|                 {chronologyEras[eraIndex].attributes?.title?.[0] | ||||
|                   ? chronologyEras[eraIndex].attributes?.title?.[0]?.title | ||||
|                   : prettySlug(chronologyEras[eraIndex].attributes?.slug)} | ||||
|               </h2> | ||||
|               <p className="whitespace-pre-line "> | ||||
|                 {chronologyEras[eraIndex].attributes?.title?.[0] | ||||
|                   ? chronologyEras[eraIndex].attributes?.title?.[0]?.description | ||||
|                   : ""} | ||||
|               </p> | ||||
|             </InsetBox> | ||||
|             {era.map((items, index) => ( | ||||
|               <Fragment key={index}> | ||||
|                 {items[0].attributes && isDefined(items[0].attributes.year) && ( | ||||
|                   <ChronologyYearComponent | ||||
|                     year={items[0].attributes.year} | ||||
|                     items={items} | ||||
|                     langui={langui} | ||||
|                   /> | ||||
|                 )} | ||||
|               </Fragment> | ||||
|             ))} | ||||
|           </Fragment> | ||||
|         ))} | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [chronologyEras, chronologyItemYearGroups, langui] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -47,89 +47,95 @@ export default function Wiki(props: Props): JSX.Element { | ||||
|     [pages, searchName] | ||||
|   ); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
|         icon={Icon.TravelExplore} | ||||
|         title={langui.wiki} | ||||
|         description={langui.wiki_description} | ||||
|       /> | ||||
| 
 | ||||
|       <TextInput | ||||
|         className="mb-6 w-full" | ||||
|         placeholder={langui.search_title ?? undefined} | ||||
|         state={searchName} | ||||
|         setState={setSearchName} | ||||
|       /> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel | ||||
|           label={langui.always_show_info} | ||||
|           input={ | ||||
|             <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|           } | ||||
|   const subPanel = useMemo( | ||||
|     () => ( | ||||
|       <SubPanel> | ||||
|         <PanelHeader | ||||
|           icon={Icon.TravelExplore} | ||||
|           title={langui.wiki} | ||||
|           description={langui.wiki_description} | ||||
|         /> | ||||
|       )} | ||||
| 
 | ||||
|       <Button | ||||
|         className="mt-8" | ||||
|         text={langui.reset_all_filters} | ||||
|         icon={Icon.Replay} | ||||
|         onClick={() => { | ||||
|           setSearchName(defaultFiltersState.searchName); | ||||
|           setKeepInfoVisible(defaultFiltersState.keepInfoVisible); | ||||
|         }} | ||||
|       /> | ||||
|       <HorizontalLine /> | ||||
|         <TextInput | ||||
|           className="mb-6 w-full" | ||||
|           placeholder={langui.search_title ?? undefined} | ||||
|           state={searchName} | ||||
|           setState={setSearchName} | ||||
|         /> | ||||
| 
 | ||||
|       {/* TODO: Langui */} | ||||
|       <p className="mb-4 font-headers text-xl">Special Pages</p> | ||||
| 
 | ||||
|       <NavOption title={langui.chronology} url="/wiki/chronology" border /> | ||||
|     </SubPanel> | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|       <div | ||||
|         className="grid grid-cols-2 items-end gap-8 | ||||
|         desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))] mobile:gap-4" | ||||
|       > | ||||
|         {/* TODO: Add to langui */} | ||||
|         {filteredPages.length === 0 && ( | ||||
|           <ContentPlaceholder | ||||
|             message={ | ||||
|               "No results. You can try changing or resetting the search parameters." | ||||
|         {hoverable && ( | ||||
|           <WithLabel | ||||
|             label={langui.always_show_info} | ||||
|             input={ | ||||
|               <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|             } | ||||
|             icon={Icon.ChevronLeft} | ||||
|           /> | ||||
|         )} | ||||
|         {filterHasAttributes(filteredPages).map((page) => ( | ||||
|           <Fragment key={page.id}> | ||||
|             <TranslatedPreviewCard | ||||
|               href={`/wiki/${page.attributes.slug}`} | ||||
|               translations={page.attributes.translations?.map( | ||||
|                 (translation) => ({ | ||||
|                   title: translation?.title, | ||||
|                   description: translation?.summary, | ||||
|                   language: translation?.language?.data?.attributes?.code, | ||||
|                 }) | ||||
|               )} | ||||
|               thumbnail={page.attributes.thumbnail?.data?.attributes} | ||||
|               thumbnailAspectRatio={"4/3"} | ||||
|               thumbnailRounded | ||||
|               thumbnailForceAspectRatio | ||||
|               languages={languages} | ||||
|               slug={page.attributes.slug} | ||||
|               keepInfoVisible={keepInfoVisible} | ||||
|               bottomChips={page.attributes.categories?.data.map( | ||||
|                 (category) => category.attributes?.short ?? "" | ||||
|               )} | ||||
| 
 | ||||
|         <Button | ||||
|           className="mt-8" | ||||
|           text={langui.reset_all_filters} | ||||
|           icon={Icon.Replay} | ||||
|           onClick={() => { | ||||
|             setSearchName(defaultFiltersState.searchName); | ||||
|             setKeepInfoVisible(defaultFiltersState.keepInfoVisible); | ||||
|           }} | ||||
|         /> | ||||
|         <HorizontalLine /> | ||||
| 
 | ||||
|         {/* TODO: Langui */} | ||||
|         <p className="mb-4 font-headers text-xl">Special Pages</p> | ||||
| 
 | ||||
|         <NavOption title={langui.chronology} url="/wiki/chronology" border /> | ||||
|       </SubPanel> | ||||
|     ), | ||||
|     [hoverable, keepInfoVisible, langui, searchName] | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = useMemo( | ||||
|     () => ( | ||||
|       <ContentPanel width={ContentPanelWidthSizes.Full}> | ||||
|         <div | ||||
|           className="grid grid-cols-2 items-end gap-8 | ||||
|         desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))] mobile:gap-4" | ||||
|         > | ||||
|           {/* TODO: Add to langui */} | ||||
|           {filteredPages.length === 0 && ( | ||||
|             <ContentPlaceholder | ||||
|               message={ | ||||
|                 "No results. You can try changing or resetting the search parameters." | ||||
|               } | ||||
|               icon={Icon.ChevronLeft} | ||||
|             /> | ||||
|           </Fragment> | ||||
|         ))} | ||||
|       </div> | ||||
|     </ContentPanel> | ||||
|           )} | ||||
|           {filterHasAttributes(filteredPages).map((page) => ( | ||||
|             <Fragment key={page.id}> | ||||
|               <TranslatedPreviewCard | ||||
|                 href={`/wiki/${page.attributes.slug}`} | ||||
|                 translations={page.attributes.translations?.map( | ||||
|                   (translation) => ({ | ||||
|                     title: translation?.title, | ||||
|                     description: translation?.summary, | ||||
|                     language: translation?.language?.data?.attributes?.code, | ||||
|                   }) | ||||
|                 )} | ||||
|                 thumbnail={page.attributes.thumbnail?.data?.attributes} | ||||
|                 thumbnailAspectRatio={"4/3"} | ||||
|                 thumbnailRounded | ||||
|                 thumbnailForceAspectRatio | ||||
|                 languages={languages} | ||||
|                 slug={page.attributes.slug} | ||||
|                 keepInfoVisible={keepInfoVisible} | ||||
|                 bottomChips={page.attributes.categories?.data.map( | ||||
|                   (category) => category.attributes?.short ?? "" | ||||
|                 )} | ||||
|               /> | ||||
|             </Fragment> | ||||
|           ))} | ||||
|         </div> | ||||
|       </ContentPanel> | ||||
|     ), | ||||
|     [filteredPages, keepInfoVisible, languages] | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint