commit
						5de174d63e
					
				| @ -50,6 +50,7 @@ module.exports = { | ||||
|     "max-classes-per-file": ["error", 1], | ||||
|     // "max-depth": ["warn", 4],
 | ||||
|     // "max-lines": "warn",
 | ||||
|     "max-len": ["warn", { code: 100 }], | ||||
|     // "max-lines-per-function": "warn",
 | ||||
|     // "max-nested-callbacks": "warn",
 | ||||
|     // "max-params": "warn",
 | ||||
|  | ||||
							
								
								
									
										21
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
									
									
									
									
								
							| @ -25,6 +25,7 @@ | ||||
| #### [Front](https://github.com/Accords-Library/accords-library.com) (this repository) | ||||
| 
 | ||||
| - Language: [TypeScript](https://www.typescriptlang.org/) | ||||
| - Framework: [Next.js](https://nextjs.org/) (React) | ||||
| - Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/) | ||||
|   - Fetch the GraphQL schema from the GraphQL back-end endpoint | ||||
|   - Read the operations and fragments stored as graphql files in the `src/graphql` folder | ||||
| @ -33,28 +34,34 @@ | ||||
|   - Support for Arbitrary React Components and Component Props! | ||||
|   - Autogenerated multi-level table of content and anchor links for the different headers | ||||
| - Styling: [Tailwind CSS](https://tailwindcss.com/) | ||||
|   - Good typographic defaults using [Tailwind/Typography](https://tailwindcss.com/docs/typography-plugin) | ||||
|   - Beside the theme declaration no CSS outside of Tailwind CSS | ||||
|   - Manually added support for scrollbar styling | ||||
|   - Support for [Material Icons](https://fonts.google.com/icons) | ||||
|   - Support for light and dark mode with a manual switch and system's selected theme by default | ||||
|   - Support for creating any arbitrary theming mode by swapping CSS variables | ||||
|   - Support for many screen sizes and resolutions | ||||
| - Framework: [Next.js](https://nextjs.org/) (React) | ||||
|   - Multilanguage support | ||||
| - State Management: [React Context](https://reactjs.org/docs/context.html) | ||||
|   - Persistent app state using LocalStorage | ||||
| - Accessibility | ||||
|   - Gestures using [react-swipeable](https://www.npmjs.com/package/react-swipeable) | ||||
|   - Keyboard hotkeys using [react-hot-keys](https://www.npmjs.com/package/react-hot-keys) | ||||
|   - Support for light and dark mode with a manual switch and system's selected theme by default | ||||
|   - Fonts can be swaped to [OpenDyslexic](https://www.npmjs.com/package/@fontsource/opendyslexic) | ||||
| - Multilingual | ||||
|   - By default, use the browser's language as the main language | ||||
|   - Fallback languages are used for content which are not available in the main language | ||||
|   - Main and fallback languages can be ordered manually by the user | ||||
|   - At the content level, the user can know which language is available | ||||
|   - Furthermore, the user can temporary select another language then the one that was automatically selected | ||||
| - SSG + ISR (Static Site Generation + Incremental Static Regeneration): | ||||
|   - The website is built before running in production | ||||
|   - Performances are great, and possibility to deploy the app using a CDN | ||||
|   - On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted. | ||||
|   - On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted | ||||
| - SEO | ||||
|   - Good defaults for the metadate and OpenGraph properties | ||||
|   - Each page can provide the thumbnail, title, description to be used | ||||
|   - Automatic generation of the sitemap using [next-sitemap](https://www.npmjs.com/package/next-sitemap) | ||||
| - Data quality testing | ||||
|   - Data from the CMS is subject to a battery of tests (about 20 warning types and 40 error types) at build time | ||||
|   - Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it. | ||||
|   - Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it | ||||
|   - Check for completeness, conformity, and integrity | ||||
| 
 | ||||
| ## Installation | ||||
|  | ||||
							
								
								
									
										2245
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2245
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										42
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								package.json
									
									
									
									
									
								
							| @ -16,39 +16,39 @@ | ||||
|     "@fontsource/material-icons": "^4.5.4", | ||||
|     "@fontsource/material-icons-rounded": "^4.5.4", | ||||
|     "@fontsource/opendyslexic": "^4.5.4", | ||||
|     "@fontsource/vollkorn": "^4.5.6", | ||||
|     "@fontsource/zen-maru-gothic": "^4.5.8", | ||||
|     "@fontsource/vollkorn": "^4.5.9", | ||||
|     "@fontsource/zen-maru-gothic": "^4.5.11", | ||||
|     "@tippyjs/react": "^4.2.6", | ||||
|     "autoprefixer": "^10.4.5", | ||||
|     "autoprefixer": "^10.4.7", | ||||
|     "graphql-request": "^4.2.0", | ||||
|     "markdown-to-jsx": "^7.1.7", | ||||
|     "next": "^12.1.2", | ||||
|     "nodemailer": "^6.7.3", | ||||
|     "react": "17.0.2", | ||||
|     "react-dom": "17.0.2", | ||||
|     "react-image-lightbox": "^5.1.4", | ||||
|     "react-swipeable": "^6.2.1", | ||||
|     "next": "^12.1.6", | ||||
|     "nodemailer": "^6.7.5", | ||||
|     "react": "18.1.0", | ||||
|     "react-dom": "18.1.0", | ||||
|     "react-hot-keys": "^2.7.2", | ||||
|     "react-swipeable": "^7.0.0", | ||||
|     "turndown": "^7.1.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@graphql-codegen/cli": "^2.6.2", | ||||
|     "@graphql-codegen/typescript": "2.4.8", | ||||
|     "@graphql-codegen/typescript-graphql-request": "^4.4.5", | ||||
|     "@graphql-codegen/typescript-operations": "^2.3.5", | ||||
|     "@types/node": "17.0.25", | ||||
|     "@graphql-codegen/typescript": "2.4.11", | ||||
|     "@graphql-codegen/typescript-graphql-request": "^4.4.8", | ||||
|     "@graphql-codegen/typescript-operations": "^2.4.0", | ||||
|     "@types/node": "17.0.33", | ||||
|     "@types/nodemailer": "^6.4.4", | ||||
|     "@types/react": "17.0.43", | ||||
|     "@types/react-dom": "^17.0.14", | ||||
|     "@types/react": "18.0.9", | ||||
|     "@types/react-dom": "^18.0.4", | ||||
|     "@types/turndown": "^5.0.1", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.20.0", | ||||
|     "@typescript-eslint/parser": "^5.20.0", | ||||
|     "eslint": "^8.14.0", | ||||
|     "eslint-config-next": "12.1.5", | ||||
|     "graphql": "^14.7.0", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.23.0", | ||||
|     "@typescript-eslint/parser": "^5.23.0", | ||||
|     "eslint": "^8.15.0", | ||||
|     "eslint-config-next": "12.1.6", | ||||
|     "graphql": "^16.5.0", | ||||
|     "next-sitemap": "^2.5.20", | ||||
|     "prettier": "^2.6.2", | ||||
|     "prettier-plugin-organize-imports": "^2.3.4", | ||||
|     "tailwindcss": "^3.0.24", | ||||
|     "typescript": "^4.6.3" | ||||
|     "typescript": "^4.6.4" | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,23 +1,19 @@ | ||||
| import Button from "components/Inputs/Button"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { UploadImageFragment } from "graphql/generated"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { prettyLanguage, prettySlug } from "helpers/formatters"; | ||||
| import { getOgImage, ImageQuality, OgImage } from "helpers/img"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useMediaMobile } from "hooks/useMediaQuery"; | ||||
| import Head from "next/head"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { | ||||
|   getOgImage, | ||||
|   OgImage, | ||||
|   prettyLanguage, | ||||
|   prettySlug, | ||||
| } from "queries/helpers"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useSwipeable } from "react-swipeable"; | ||||
| import { ImageQuality } from "./Img"; | ||||
| import OrderableList from "./Inputs/OrderableList"; | ||||
| import Select from "./Inputs/Select"; | ||||
| import MainPanel from "./Panels/MainPanel"; | ||||
| import Popup from "./Popup"; | ||||
| import { OrderableList } from "./Inputs/OrderableList"; | ||||
| import { Select } from "./Inputs/Select"; | ||||
| import { MainPanel } from "./Panels/MainPanel"; | ||||
| import { Popup } from "./Popup"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   subPanel?: React.ReactNode; | ||||
| @ -29,8 +25,19 @@ interface Props extends AppStaticProps { | ||||
|   description?: string; | ||||
| } | ||||
| 
 | ||||
| export default function AppLayout(props: Props): JSX.Element { | ||||
|   const { langui, currencies, languages, subPanel, contentPanel } = props; | ||||
| export function AppLayout(props: Immutable<Props>): JSX.Element { | ||||
|   const { | ||||
|     langui, | ||||
|     currencies, | ||||
|     languages, | ||||
|     subPanel, | ||||
|     contentPanel, | ||||
|     thumbnail, | ||||
|     title, | ||||
|     navTitle, | ||||
|     description, | ||||
|     subPanelIcon, | ||||
|   } = props; | ||||
|   const router = useRouter(); | ||||
|   const isMobile = useMediaMobile(); | ||||
|   const appLayout = useAppLayout(); | ||||
| @ -39,19 +46,23 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
| 
 | ||||
|   const handlers = useSwipeable({ | ||||
|     onSwipedLeft: (SwipeEventData) => { | ||||
|       if (SwipeEventData.velocity < sensibilitySwipe) return; | ||||
|       if (appLayout.mainPanelOpen) { | ||||
|         appLayout.setMainPanelOpen(false); | ||||
|       } else if (subPanel && contentPanel) { | ||||
|         appLayout.setSubPanelOpen(true); | ||||
|       if (appLayout.menuGestures) { | ||||
|         if (SwipeEventData.velocity < sensibilitySwipe) return; | ||||
|         if (appLayout.mainPanelOpen) { | ||||
|           appLayout.setMainPanelOpen(false); | ||||
|         } else if (subPanel && contentPanel) { | ||||
|           appLayout.setSubPanelOpen(true); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     onSwipedRight: (SwipeEventData) => { | ||||
|       if (SwipeEventData.velocity < sensibilitySwipe) return; | ||||
|       if (appLayout.subPanelOpen) { | ||||
|         appLayout.setSubPanelOpen(false); | ||||
|       } else { | ||||
|         appLayout.setMainPanelOpen(true); | ||||
|       if (appLayout.menuGestures) { | ||||
|         if (SwipeEventData.velocity < sensibilitySwipe) return; | ||||
|         if (appLayout.subPanelOpen) { | ||||
|           appLayout.setSubPanelOpen(false); | ||||
|         } else { | ||||
|           appLayout.setMainPanelOpen(true); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
| @ -59,8 +70,8 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|   const turnSubIntoContent = subPanel && !contentPanel; | ||||
| 
 | ||||
|   const titlePrefix = "Accord’s Library"; | ||||
|   const metaImage: OgImage = props.thumbnail | ||||
|     ? getOgImage(ImageQuality.Og, props.thumbnail) | ||||
|   const metaImage: OgImage = thumbnail | ||||
|     ? getOgImage(ImageQuality.Og, thumbnail) | ||||
|     : { | ||||
|         image: "/default_og.jpg", | ||||
|         width: 1200, | ||||
| @ -68,9 +79,9 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|         alt: "Accord's Library Logo", | ||||
|       }; | ||||
|   const ogTitle = | ||||
|     props.title ?? props.navTitle ?? prettySlug(router.asPath.split("/").pop()); | ||||
|     title ?? navTitle ?? prettySlug(router.asPath.split("/").pop()); | ||||
| 
 | ||||
|   const metaDescription = props.description ?? langui.default_description ?? ""; | ||||
|   const metaDescription = description ?? langui.default_description ?? ""; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     document.getElementsByTagName("html")[0].style.fontSize = `${ | ||||
| @ -115,7 +126,7 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|   }, [currencySelect]); | ||||
| 
 | ||||
|   let gridCol = ""; | ||||
|   if (props.subPanel) { | ||||
|   if (subPanel) { | ||||
|     if (appLayout.mainPanelReduced) { | ||||
|       gridCol = "grid-cols-[6rem_20rem_1fr]"; | ||||
|     } else { | ||||
| @ -140,7 +151,9 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|     > | ||||
|       <div | ||||
|         {...handlers} | ||||
|         className={`fixed inset-0 touch-pan-y p-0 m-0 bg-light text-black grid [grid-template-areas:'main_sub_content'] ${gridCol} mobile:grid-cols-[1fr] mobile:grid-rows-[1fr_5rem] mobile:[grid-template-areas:'content''navbar']`} | ||||
|         className={`fixed inset-0 touch-pan-y p-0 m-0 bg-light text-black grid
 | ||||
|         [grid-template-areas:'main_sub_content'] ${gridCol} mobile:grid-cols-[1fr] | ||||
|         mobile:grid-rows-[1fr_5rem] mobile:[grid-template-areas:'content''navbar']`}
 | ||||
|       > | ||||
|         <Head> | ||||
|           <title>{`${titlePrefix} - ${ogTitle}`}</title> | ||||
| @ -172,7 +185,8 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
| 
 | ||||
|         {/* Background when navbar is opened */} | ||||
|         <div | ||||
|           className={`[grid-area:content] mobile:z-10 absolute inset-0 transition-[backdrop-filter] duration-500 ${ | ||||
|           className={`[grid-area:content] mobile:z-10 absolute
 | ||||
|           inset-0 transition-[backdrop-filter] duration-500 ${ | ||||
|             (appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile | ||||
|               ? "[backdrop-filter:blur(2px)]" | ||||
|               : "pointer-events-none touch-none " | ||||
| @ -201,7 +215,10 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|             contentPanel | ||||
|           ) : ( | ||||
|             <div className="grid place-content-center h-full"> | ||||
|               <div className="text-dark border-dark border-2 border-dotted rounded-2xl p-8 grid grid-flow-col place-items-center gap-9 opacity-40"> | ||||
|               <div | ||||
|                 className="text-dark border-dark border-2 border-dotted rounded-2xl | ||||
|               p-8 grid grid-flow-col place-items-center gap-9 opacity-40" | ||||
|               > | ||||
|                 <p className="text-4xl">❮</p> | ||||
|                 <p className="text-2xl w-64">{langui.select_option_sidebar}</p> | ||||
|               </div> | ||||
| @ -212,7 +229,10 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|         {/* Sub panel */} | ||||
|         {subPanel && ( | ||||
|           <div | ||||
|             className={`[grid-area:sub] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%] mobile:justify-self-end border-r-[1px] mobile:border-r-0 mobile:border-l-[1px] border-black border-dotted overflow-y-scroll webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots
 | ||||
|             className={`[grid-area:sub] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%]
 | ||||
|             mobile:justify-self-end border-r-[1px] mobile:border-r-0 mobile:border-l-[1px] | ||||
|             border-black border-dotted overflow-y-scroll webkit-scrollbar:w-0 | ||||
|             [scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots | ||||
|           ${ | ||||
|             turnSubIntoContent | ||||
|               ? "mobile:border-l-0 mobile:w-full" | ||||
| @ -225,14 +245,21 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
| 
 | ||||
|         {/* Main panel */} | ||||
|         <div | ||||
|           className={`[grid-area:main] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%] mobile:justify-self-start border-r-[1px] border-black border-dotted overflow-y-scroll webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots
 | ||||
|         ${appLayout.mainPanelOpen ? "" : "mobile:-translate-x-full"}`}
 | ||||
|           className={`[grid-area:main] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%]
 | ||||
|           mobile:justify-self-start border-r-[1px] border-black border-dotted overflow-y-scroll | ||||
|           webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light | ||||
|           texture-paper-dots ${ | ||||
|             appLayout.mainPanelOpen ? "" : "mobile:-translate-x-full" | ||||
|           }`}
 | ||||
|         > | ||||
|           <MainPanel langui={langui} /> | ||||
|         </div> | ||||
| 
 | ||||
|         {/* Navbar */} | ||||
|         <div className="[grid-area:navbar] border-t-[1px] border-black border-dotted grid grid-cols-[5rem_1fr_5rem] place-items-center desktop:hidden bg-light texture-paper-dots"> | ||||
|         <div | ||||
|           className="[grid-area:navbar] border-t-[1px] border-black border-dotted grid | ||||
|           grid-cols-[5rem_1fr_5rem] place-items-center desktop:hidden bg-light texture-paper-dots" | ||||
|         > | ||||
|           <span | ||||
|             className="material-icons mt-[.1em] cursor-pointer" | ||||
|             onClick={() => { | ||||
| @ -261,8 +288,8 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|             {subPanel && !turnSubIntoContent | ||||
|               ? appLayout.subPanelOpen | ||||
|                 ? "close" | ||||
|                 : props.subPanelIcon | ||||
|                 ? props.subPanelIcon | ||||
|                 : subPanelIcon | ||||
|                 ? subPanelIcon | ||||
|                 : "tune" | ||||
|               : ""} | ||||
|           </span> | ||||
| @ -274,7 +301,10 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|         > | ||||
|           <h2 className="text-2xl">{langui.settings}</h2> | ||||
| 
 | ||||
|           <div className="mt-4 grid gap-16 justify-items-center text-center desktop:grid-cols-[auto_auto]"> | ||||
|           <div | ||||
|             className="mt-4 grid gap-16 justify-items-center | ||||
|             text-center desktop:grid-cols-[auto_auto]" | ||||
|           > | ||||
|             {router.locales && ( | ||||
|               <div> | ||||
|                 <h3 className="text-xl">{langui.languages}</h3> | ||||
| @ -295,6 +325,12 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|                             ]) | ||||
|                           ) | ||||
|                     } | ||||
|                     insertLabels={ | ||||
|                       new Map([ | ||||
|                         [0, langui.primary_language], | ||||
|                         [1, langui.secondary_language], | ||||
|                       ]) | ||||
|                     } | ||||
|                     onChange={(items) => { | ||||
|                       const preferredLanguages = [...items].map( | ||||
|                         ([code]) => code | ||||
| @ -437,6 +473,7 @@ export default function AppLayout(props: Props): JSX.Element { | ||||
|                       (event.target as HTMLInputElement).value | ||||
|                     ) | ||||
|                   } | ||||
|                   value={appLayout.playerName} | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
|  | ||||
| @ -1,12 +1,16 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   className?: string; | ||||
|   children: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| export default function Chip(props: Props): JSX.Element { | ||||
| export function Chip(props: Immutable<Props>): JSX.Element { | ||||
|   return ( | ||||
|     <div | ||||
|       className={`grid place-content-center place-items-center text-xs pb-[0.14rem] whitespace-nowrap px-1.5 border-[1px] rounded-full opacity-70 transition-[color,_opacity,_border-color] hover:opacity-100 ${props.className}`} | ||||
|       className={`grid place-content-center place-items-center text-xs pb-[0.14rem]
 | ||||
|       whitespace-nowrap px-1.5 border-[1px] rounded-full opacity-70 | ||||
|       transition-[color,_opacity,_border-color] hover:opacity-100 ${props.className}`}
 | ||||
|     > | ||||
|       {props.children} | ||||
|     </div> | ||||
|  | ||||
| @ -1,8 +1,10 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   className?: string; | ||||
| } | ||||
| 
 | ||||
| export default function HorizontalLine(props: Props): JSX.Element { | ||||
| export function HorizontalLine(props: Immutable<Props>): JSX.Element { | ||||
|   return ( | ||||
|     <div | ||||
|       className={`h-0 w-full my-8 border-t-[3px] border-dotted border-black ${props.className}`} | ||||
|  | ||||
| @ -1,106 +1,43 @@ | ||||
| import { UploadImageFragment } from "graphql/generated"; | ||||
| import Image, { ImageProps } from "next/image"; | ||||
| import { getAssetURL, getImgSizesByQuality, ImageQuality } from "helpers/img"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { ImageProps } from "next/image"; | ||||
| import { MouseEventHandler } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   className?: string; | ||||
|   image?: UploadImageFragment | string; | ||||
|   quality?: ImageQuality; | ||||
|   alt?: ImageProps["alt"]; | ||||
|   layout?: ImageProps["layout"]; | ||||
|   objectFit?: ImageProps["objectFit"]; | ||||
|   priority?: ImageProps["priority"]; | ||||
|   onClick?: MouseEventHandler<HTMLImageElement>; | ||||
| } | ||||
| 
 | ||||
| export default function Img(props: Props): JSX.Element { | ||||
|   if (typeof props.image === "string") { | ||||
| export function Img(props: Immutable<Props>): JSX.Element { | ||||
|   const { | ||||
|     className, | ||||
|     image, | ||||
|     quality = ImageQuality.Small, | ||||
|     alt, | ||||
|     onClick, | ||||
|   } = props; | ||||
| 
 | ||||
|   if (typeof image === "string") { | ||||
|     return ( | ||||
|       <img className={className} src={image} alt={alt ?? ""} loading="lazy" /> | ||||
|     ); | ||||
|   } else if (image?.width && image.height) { | ||||
|     const imgSize = getImgSizesByQuality(image.width, image.height, quality); | ||||
|     return ( | ||||
|       <img | ||||
|         className={props.className} | ||||
|         src={props.image} | ||||
|         alt={props.alt ?? ""} | ||||
|       /> | ||||
|     ); | ||||
|   } else if (props.image?.width && props.image.height) { | ||||
|     const imgSize = getImgSizesByQuality( | ||||
|       props.image.width, | ||||
|       props.image.height, | ||||
|       props.quality ?? ImageQuality.Small | ||||
|     ); | ||||
|     return ( | ||||
|       <Image | ||||
|         className={props.className} | ||||
|         src={getAssetURL( | ||||
|           props.image.url, | ||||
|           props.quality ? props.quality : ImageQuality.Small | ||||
|         )} | ||||
|         alt={props.alt ?? props.image.alternativeText ?? ""} | ||||
|         width={props.layout === "fill" ? undefined : imgSize.width} | ||||
|         height={props.layout === "fill" ? undefined : imgSize.height} | ||||
|         layout={props.layout} | ||||
|         objectFit={props.objectFit} | ||||
|         priority={props.priority} | ||||
|         unoptimized | ||||
|         className={className} | ||||
|         src={getAssetURL(image.url, quality)} | ||||
|         alt={alt ?? image.alternativeText ?? ""} | ||||
|         width={imgSize.width} | ||||
|         height={imgSize.height} | ||||
|         loading="lazy" | ||||
|         onClick={onClick} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
|   return <></>; | ||||
| } | ||||
| 
 | ||||
| export enum ImageQuality { | ||||
|   Small = "small", | ||||
|   Medium = "medium", | ||||
|   Large = "large", | ||||
|   Og = "og", | ||||
| } | ||||
| 
 | ||||
| export function getAssetFilename(path: string): string { | ||||
|   let result = path.split("/"); | ||||
|   result = result[result.length - 1].split("."); | ||||
|   result = result | ||||
|     .splice(0, result.length - 1) | ||||
|     .join(".") | ||||
|     .split("_"); | ||||
|   return result[0]; | ||||
| } | ||||
| 
 | ||||
| export function getAssetURL(url: string, quality: ImageQuality): string { | ||||
|   let newUrl = url; | ||||
|   newUrl = newUrl.replace(/^\/uploads/u, `/${quality}`); | ||||
|   newUrl = newUrl.replace(/.jpg$/u, ".webp"); | ||||
|   newUrl = newUrl.replace(/.jpeg$/u, ".webp"); | ||||
|   newUrl = newUrl.replace(/.png$/u, ".webp"); | ||||
|   if (quality === ImageQuality.Og) newUrl = newUrl.replace(/.webp$/u, ".jpg"); | ||||
|   return process.env.NEXT_PUBLIC_URL_IMG + newUrl; | ||||
| } | ||||
| 
 | ||||
| export function getImgSizesByMaxSize( | ||||
|   width: number, | ||||
|   height: number, | ||||
|   maxSize: number | ||||
| ): { width: number; height: number } { | ||||
|   if (width > height) { | ||||
|     if (width < maxSize) return { width: width, height: height }; | ||||
|     return { width: maxSize, height: (height / width) * maxSize }; | ||||
|   } | ||||
|   if (height < maxSize) return { width: width, height: height }; | ||||
|   return { width: (width / height) * maxSize, height: maxSize }; | ||||
| } | ||||
| 
 | ||||
| export function getImgSizesByQuality( | ||||
|   width: number, | ||||
|   height: number, | ||||
|   quality: ImageQuality | ||||
| ): { width: number; height: number } { | ||||
|   switch (quality) { | ||||
|     case ImageQuality.Og: | ||||
|       return getImgSizesByMaxSize(width, height, 512); | ||||
|     case ImageQuality.Small: | ||||
|       return getImgSizesByMaxSize(width, height, 512); | ||||
|     case ImageQuality.Medium: | ||||
|       return getImgSizesByMaxSize(width, height, 1024); | ||||
|     case ImageQuality.Large: | ||||
|       return getImgSizesByMaxSize(width, height, 2048); | ||||
|     default: | ||||
|       return { width: 0, height: 0 }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { MouseEventHandler } from "react"; | ||||
| 
 | ||||
| @ -14,7 +15,7 @@ interface Props { | ||||
|   badgeNumber?: number; | ||||
| } | ||||
| 
 | ||||
| export default function Button(props: Props): JSX.Element { | ||||
| export function Button(props: Immutable<Props>): JSX.Element { | ||||
|   const { | ||||
|     draggable, | ||||
|     id, | ||||
| @ -39,11 +40,15 @@ export default function Button(props: Props): JSX.Element { | ||||
|       transition-all select-none hover:[--opacityBadge:0] --opacityBadge:100 ${className} ${ | ||||
|         active | ||||
|           ? "text-light bg-black drop-shadow-black-lg !border-black cursor-not-allowed" | ||||
|           : "cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg active:bg-black active:text-light active:drop-shadow-black-lg active:border-black" | ||||
|           : `cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg
 | ||||
|           active:bg-black active:text-light active:drop-shadow-black-lg active:border-black` | ||||
|       }`}
 | ||||
|     > | ||||
|       {badgeNumber && ( | ||||
|         <div className="opacity-[var(--opacityBadge)] transition-opacity grid place-items-center absolute -top-3 -right-2 bg-dark w-8 h-8 text-light font-bold rounded-full"> | ||||
|         <div | ||||
|           className="opacity-[var(--opacityBadge)] transition-opacity grid place-items-center | ||||
|           absolute -top-3 -right-2 bg-dark w-8 h-8 text-light font-bold rounded-full" | ||||
|         > | ||||
|           {badgeNumber} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { prettyLanguage } from "queries/helpers"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { prettyLanguage } from "helpers/formatters"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { Dispatch, SetStateAction } from "react"; | ||||
| import ToolTip from "../ToolTip"; | ||||
| import Button from "./Button"; | ||||
| import { ToolTip } from "../ToolTip"; | ||||
| import { Button } from "./Button"; | ||||
| 
 | ||||
| interface Props { | ||||
|   className?: string; | ||||
| @ -12,7 +13,7 @@ interface Props { | ||||
|   setLocalesIndex: Dispatch<SetStateAction<number | undefined>>; | ||||
| } | ||||
| 
 | ||||
| export default function LanguageSwitcher(props: Props): JSX.Element { | ||||
| export function LanguageSwitcher(props: Immutable<Props>): JSX.Element { | ||||
|   const { locales, className, localesIndex, setLocalesIndex } = props; | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| import { arrayMove } from "queries/helpers"; | ||||
| import { arrayMove } from "helpers/others"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   className?: string; | ||||
|   items: Map<string, string>; | ||||
|   insertLabels?: Map<number, string | null | undefined>; | ||||
|   onChange?: (items: Map<string, string>) => void; | ||||
| } | ||||
| 
 | ||||
| export default function OrderableList(props: Props): JSX.Element { | ||||
| export function OrderableList(props: Immutable<Props>): JSX.Element { | ||||
|   const [items, setItems] = useState<Map<string, string>>(props.items); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
| @ -24,12 +26,8 @@ export default function OrderableList(props: Props): JSX.Element { | ||||
|     <div className="grid gap-2"> | ||||
|       {[...items].map(([key, value], index) => ( | ||||
|         <> | ||||
|           {index === 0 ? ( | ||||
|             <p>Primary language</p> | ||||
|           ) : index === 1 ? ( | ||||
|             <p>Secondary languages</p> | ||||
|           ) : ( | ||||
|             "" | ||||
|           {props.insertLabels?.get(index) && ( | ||||
|             <p>{props.insertLabels.get(index)}</p> | ||||
|           )} | ||||
|           <div | ||||
|             onDragStart={(event) => { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { Dispatch, SetStateAction } from "react"; | ||||
| import Button from "./Button"; | ||||
| import { Button } from "./Button"; | ||||
| 
 | ||||
| interface Props { | ||||
|   className?: string; | ||||
| @ -8,7 +9,7 @@ interface Props { | ||||
|   setPage: Dispatch<SetStateAction<number>>; | ||||
| } | ||||
| 
 | ||||
| export default function PageSelector(props: Props): JSX.Element { | ||||
| export function PageSelector(props: Immutable<Props>): JSX.Element { | ||||
|   const { page, setPage, maxPage } = props; | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { Dispatch, SetStateAction, useState } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
| @ -9,7 +10,7 @@ interface Props { | ||||
|   className?: string; | ||||
| } | ||||
| 
 | ||||
| export default function Select(props: Props): JSX.Element { | ||||
| export function Select(props: Immutable<Props>): JSX.Element { | ||||
|   const [opened, setOpened] = useState(false); | ||||
| 
 | ||||
|   return ( | ||||
| @ -19,7 +20,9 @@ export default function Select(props: Props): JSX.Element { | ||||
|       } ${props.className}`}
 | ||||
|     > | ||||
|       <div | ||||
|         className={`outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent] bg-light rounded-[1em] p-1 grid grid-flow-col grid-cols-[1fr_auto_auto] place-items-center cursor-pointer hover:bg-mid transition-all ${ | ||||
|         className={`outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent]
 | ||||
|         bg-light rounded-[1em] p-1 grid grid-flow-col grid-cols-[1fr_auto_auto] place-items-center | ||||
|         cursor-pointer hover:bg-mid transition-all ${ | ||||
|           opened && "outline-[transparent] rounded-b-none" | ||||
|         }`}
 | ||||
|       > | ||||
| @ -47,7 +50,8 @@ export default function Select(props: Props): JSX.Element { | ||||
|           <> | ||||
|             {index !== props.state && ( | ||||
|               <div | ||||
|                 className="bg-light hover:bg-mid transition-colors cursor-pointer p-1 last-of-type:rounded-b-[1em]" | ||||
|                 className="bg-light hover:bg-mid transition-colors | ||||
|                 cursor-pointer p-1 last-of-type:rounded-b-[1em]" | ||||
|                 key={index} | ||||
|                 id={option} | ||||
|                 onClick={() => { | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { Dispatch, SetStateAction } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
| @ -6,18 +7,20 @@ interface Props { | ||||
|   className?: string; | ||||
| } | ||||
| 
 | ||||
| export default function Switch(props: Props): JSX.Element { | ||||
| export function Switch(props: Immutable<Props>): JSX.Element { | ||||
|   return ( | ||||
|     <div | ||||
|       className={`h-6 w-12 rounded-full border-2 border-mid grid transition-colors relative cursor-pointer ${ | ||||
|         props.className | ||||
|       } ${props.state ? "bg-mid" : "bg-light"}`}
 | ||||
|       className={`h-6 w-12 rounded-full border-2 border-mid grid
 | ||||
|       transition-colors relative cursor-pointer ${props.className} ${ | ||||
|         props.state ? "bg-mid" : "bg-light" | ||||
|       }`}
 | ||||
|       onClick={() => { | ||||
|         props.setState(!props.state); | ||||
|       }} | ||||
|     > | ||||
|       <div | ||||
|         className={`bg-dark aspect-square rounded-full absolute top-0 bottom-0 left-0 transition-transform ${ | ||||
|         className={`bg-dark aspect-square rounded-full absolute
 | ||||
|         top-0 bottom-0 left-0 transition-transform ${ | ||||
|           props.state && "translate-x-[115%]" | ||||
|         }`}
 | ||||
|       ></div> | ||||
|  | ||||
| @ -1,10 +1,12 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   className?: string; | ||||
|   children: React.ReactNode; | ||||
|   id?: string; | ||||
| } | ||||
| 
 | ||||
| export default function InsetBox(props: Props): JSX.Element { | ||||
| export function InsetBox(props: Immutable<Props>): JSX.Element { | ||||
|   return ( | ||||
|     <div | ||||
|       id={props.id} | ||||
|  | ||||
| @ -1,26 +1,24 @@ | ||||
| import Chip from "components/Chip"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { GetLibraryItemQuery } from "graphql/generated"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { prettyinlineTitle, prettySlug } from "queries/helpers"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { prettyinlineTitle, prettySlug } from "helpers/formatters"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   content: Exclude< | ||||
|     Exclude< | ||||
|       Exclude< | ||||
|         GetLibraryItemQuery["libraryItems"], | ||||
|         null | undefined | ||||
|       >["data"][number]["attributes"], | ||||
|       null | undefined | ||||
|     >["contents"], | ||||
|     null | undefined | ||||
|   content: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         GetLibraryItemQuery["libraryItems"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["contents"] | ||||
|   >["data"][number]; | ||||
|   parentSlug: string; | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| export default function ContentLine(props: Props): JSX.Element { | ||||
| export function ContentLine(props: Immutable<Props>): JSX.Element { | ||||
|   const { content, langui, parentSlug } = props; | ||||
| 
 | ||||
|   const [opened, setOpened] = useState(false); | ||||
| @ -32,15 +30,19 @@ export default function ContentLine(props: Props): JSX.Element { | ||||
|           opened && "bg-mid shadow-inner-sm shadow-shade h-auto py-3 my-2" | ||||
|         }`}
 | ||||
|       > | ||||
|         <div className="grid gap-4 place-items-center grid-cols-[auto_auto_1fr_auto_12ch] thin:grid-cols-[auto_auto_1fr_auto]"> | ||||
|         <div | ||||
|           className="grid gap-4 place-items-center | ||||
|         grid-cols-[auto_auto_1fr_auto_12ch] thin:grid-cols-[auto_auto_1fr_auto]" | ||||
|         > | ||||
|           <a> | ||||
|             <h3 className="cursor-pointer" onClick={() => setOpened(!opened)}> | ||||
|               {content.attributes.content?.data?.attributes?.titles?.[0] | ||||
|               {content.attributes.content?.data?.attributes?.translations?.[0] | ||||
|                 ? prettyinlineTitle( | ||||
|                     content.attributes.content.data.attributes.titles[0] | ||||
|                     content.attributes.content.data.attributes.translations[0] | ||||
|                       ?.pre_title, | ||||
|                     content.attributes.content.data.attributes.titles[0]?.title, | ||||
|                     content.attributes.content.data.attributes.titles[0] | ||||
|                     content.attributes.content.data.attributes.translations[0] | ||||
|                       ?.title, | ||||
|                     content.attributes.content.data.attributes.translations[0] | ||||
|                       ?.subtitle | ||||
|                   ) | ||||
|                 : prettySlug(content.attributes.slug, props.parentSlug)} | ||||
|  | ||||
| @ -1,76 +1,55 @@ | ||||
| import Chip from "components/Chip"; | ||||
| import Img, { | ||||
|   getAssetFilename, | ||||
|   getAssetURL, | ||||
|   ImageQuality, | ||||
| } from "components/Img"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import RecorderChip from "components/RecorderChip"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Img } from "components/Img"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { RecorderChip } from "components/RecorderChip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { GetLibraryItemScansQuery } from "graphql/generated"; | ||||
| import useSmartLanguage from "hooks/useSmartLanguage"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { getStatusDescription, isInteger } from "queries/helpers"; | ||||
| import { Dispatch, SetStateAction } from "react"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { isInteger } from "helpers/numbers"; | ||||
| import { getStatusDescription } from "helpers/others"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| interface Props { | ||||
|   setLightboxOpen: Dispatch<SetStateAction<boolean>>; | ||||
|   setLightboxImages: Dispatch<SetStateAction<string[]>>; | ||||
|   setLightboxIndex: Dispatch<SetStateAction<number>>; | ||||
|   scanSet: Exclude< | ||||
|     Exclude< | ||||
|       Exclude< | ||||
|         Exclude< | ||||
|           Exclude< | ||||
|             GetLibraryItemScansQuery["libraryItems"], | ||||
|             null | undefined | ||||
|           >["data"][number]["attributes"], | ||||
|           null | undefined | ||||
|         >["contents"], | ||||
|         null | undefined | ||||
|       >["data"][number]["attributes"], | ||||
|       null | undefined | ||||
|     >["scan_set"], | ||||
|     null | undefined | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
|   scanSet: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         NonNullable< | ||||
|           NonNullable< | ||||
|             GetLibraryItemScansQuery["libraryItems"] | ||||
|           >["data"][number]["attributes"] | ||||
|         >["contents"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["scan_set"] | ||||
|   >; | ||||
|   slug: string; | ||||
|   title: string; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   langui: AppStaticProps["langui"]; | ||||
|   content: Exclude< | ||||
|     Exclude< | ||||
|       Exclude< | ||||
|         Exclude< | ||||
|           GetLibraryItemScansQuery["libraryItems"], | ||||
|           null | undefined | ||||
|         >["data"][number]["attributes"], | ||||
|         null | undefined | ||||
|       >["contents"], | ||||
|       null | undefined | ||||
|     >["data"][number]["attributes"], | ||||
|     null | undefined | ||||
|   content: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         NonNullable< | ||||
|           GetLibraryItemScansQuery["libraryItems"] | ||||
|         >["data"][number]["attributes"] | ||||
|       >["contents"] | ||||
|     >["data"][number]["attributes"] | ||||
|   >["content"]; | ||||
| } | ||||
| 
 | ||||
| export default function ScanSet(props: Props): JSX.Element { | ||||
|   const { | ||||
|     setLightboxOpen, | ||||
|     setLightboxImages, | ||||
|     setLightboxIndex, | ||||
|     scanSet, | ||||
|     slug, | ||||
|     title, | ||||
|     languages, | ||||
|     langui, | ||||
|     content, | ||||
|   } = props; | ||||
| export function ScanSet(props: Immutable<Props>): JSX.Element { | ||||
|   const { openLightBox, scanSet, slug, title, languages, langui, content } = | ||||
|     props; | ||||
| 
 | ||||
|   const [selectedScan, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: scanSet, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item?.language?.data?.attributes?.code, | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|     transform: (item) => { | ||||
|       item?.pages?.data.sort((a, b) => { | ||||
|       const newItem = { ...item } as NonNullable<Props["scanSet"][number]>; | ||||
|       newItem.pages?.data.sort((a, b) => { | ||||
|         if (a.attributes?.url && b.attributes?.url) { | ||||
|           let aName = getAssetFilename(a.attributes.url); | ||||
|           let bName = getAssetFilename(b.attributes.url); | ||||
| @ -93,7 +72,7 @@ export default function ScanSet(props: Props): JSX.Element { | ||||
|         } | ||||
|         return 0; | ||||
|       }); | ||||
|       return item; | ||||
|       return newItem; | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
| @ -101,7 +80,10 @@ export default function ScanSet(props: Props): JSX.Element { | ||||
|     <> | ||||
|       {selectedScan && ( | ||||
|         <div> | ||||
|           <div className="flex flex-row flex-wrap place-items-center gap-6 text-base pt-10 first-of-type:pt-0"> | ||||
|           <div | ||||
|             className="flex flex-row flex-wrap place-items-center | ||||
|           gap-6 text-base pt-10 first-of-type:pt-0" | ||||
|           > | ||||
|             <h2 id={slug} className="text-2xl"> | ||||
|               {title} | ||||
|             </h2> | ||||
| @ -198,11 +180,16 @@ export default function ScanSet(props: Props): JSX.Element { | ||||
|             )} | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"> | ||||
|           <div | ||||
|             className="grid gap-8 items-end mobile:grid-cols-2 | ||||
|             desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] | ||||
|             pb-12 border-b-[3px] border-dotted last-of-type:border-0" | ||||
|           > | ||||
|             {selectedScan.pages?.data.map((page, index) => ( | ||||
|               <div | ||||
|                 key={page.id} | ||||
|                 className="drop-shadow-shade-lg hover:scale-[1.02] cursor-pointer transition-transform" | ||||
|                 className="drop-shadow-shade-lg hover:scale-[1.02] | ||||
|                 cursor-pointer transition-transform" | ||||
|                 onClick={() => { | ||||
|                   const images: string[] = []; | ||||
|                   selectedScan.pages?.data.map((image) => { | ||||
| @ -211,9 +198,7 @@ export default function ScanSet(props: Props): JSX.Element { | ||||
|                         getAssetURL(image.attributes.url, ImageQuality.Large) | ||||
|                       ); | ||||
|                   }); | ||||
|                   setLightboxOpen(true); | ||||
|                   setLightboxImages(images); | ||||
|                   setLightboxIndex(index); | ||||
|                   openLightBox(images, index); | ||||
|                 }} | ||||
|               > | ||||
|                 {page.attributes && ( | ||||
|  | ||||
| @ -1,48 +1,37 @@ | ||||
| import Chip from "components/Chip"; | ||||
| import Img, { getAssetURL, ImageQuality } from "components/Img"; | ||||
| import RecorderChip from "components/RecorderChip"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Img } from "components/Img"; | ||||
| import { RecorderChip } from "components/RecorderChip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { | ||||
|   GetLibraryItemScansQuery, | ||||
|   UploadImageFragment, | ||||
| } from "graphql/generated"; | ||||
| import useSmartLanguage from "hooks/useSmartLanguage"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { getStatusDescription } from "queries/helpers"; | ||||
| import { Dispatch, SetStateAction } from "react"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { getStatusDescription } from "helpers/others"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| 
 | ||||
| interface Props { | ||||
|   setLightboxOpen: Dispatch<SetStateAction<boolean>>; | ||||
|   setLightboxImages: Dispatch<SetStateAction<string[]>>; | ||||
|   setLightboxIndex: Dispatch<SetStateAction<number>>; | ||||
|   images: Exclude< | ||||
|     Exclude< | ||||
|       Exclude< | ||||
|         GetLibraryItemScansQuery["libraryItems"], | ||||
|         null | undefined | ||||
|       >["data"][number]["attributes"], | ||||
|       null | undefined | ||||
|     >["images"], | ||||
|     null | undefined | ||||
|   openLightBox: (images: string[], index?: number) => void; | ||||
|   images: NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         GetLibraryItemScansQuery["libraryItems"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["images"] | ||||
|   >; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| export default function ScanSetCover(props: Props): JSX.Element { | ||||
|   const { | ||||
|     setLightboxOpen, | ||||
|     setLightboxImages, | ||||
|     setLightboxIndex, | ||||
|     images, | ||||
|     languages, | ||||
|     langui, | ||||
|   } = props; | ||||
| export function ScanSetCover(props: Immutable<Props>): JSX.Element { | ||||
|   const { openLightBox, images, languages, langui } = props; | ||||
| 
 | ||||
|   const [selectedScan, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: images, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item?.language?.data?.attributes?.code, | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|   }); | ||||
| 
 | ||||
|   const coverImages: UploadImageFragment[] = []; | ||||
| @ -64,7 +53,10 @@ export default function ScanSetCover(props: Props): JSX.Element { | ||||
|       <> | ||||
|         {selectedScan && ( | ||||
|           <div> | ||||
|             <div className="flex flex-row flex-wrap place-items-center gap-6 text-base pt-10 first-of-type:pt-0"> | ||||
|             <div | ||||
|               className="flex flex-row flex-wrap place-items-center | ||||
|               gap-6 text-base pt-10 first-of-type:pt-0" | ||||
|             > | ||||
|               <h2 id={"cover"} className="text-2xl"> | ||||
|                 {"Cover"} | ||||
|               </h2> | ||||
| @ -149,20 +141,23 @@ export default function ScanSetCover(props: Props): JSX.Element { | ||||
|                 )} | ||||
|             </div> | ||||
| 
 | ||||
|             <div className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"> | ||||
|             <div | ||||
|               className="grid gap-8 items-end mobile:grid-cols-2 | ||||
|               desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] | ||||
|               pb-12 border-b-[3px] border-dotted last-of-type:border-0" | ||||
|             > | ||||
|               {coverImages.map((image, index) => ( | ||||
|                 <div | ||||
|                   key={image.url} | ||||
|                   className="drop-shadow-shade-lg hover:scale-[1.02] cursor-pointer transition-transform" | ||||
|                   className="drop-shadow-shade-lg hover:scale-[1.02] | ||||
|                   cursor-pointer transition-transform" | ||||
|                   onClick={() => { | ||||
|                     const imgs: string[] = []; | ||||
|                     coverImages.map((img) => { | ||||
|                       if (img.url) | ||||
|                         imgs.push(getAssetURL(img.url, ImageQuality.Large)); | ||||
|                     }); | ||||
|                     setLightboxOpen(true); | ||||
|                     setLightboxImages(imgs); | ||||
|                     setLightboxIndex(index); | ||||
|                     openLightBox(imgs, index); | ||||
|                   }} | ||||
|                 > | ||||
|                   <Img image={image} quality={ImageQuality.Small} /> | ||||
|  | ||||
| @ -1,6 +1,10 @@ | ||||
| import { useMediaMobile } from "hooks/useMediaQuery"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { Dispatch, SetStateAction } from "react"; | ||||
| import Lightbox from "react-image-lightbox"; | ||||
| import Hotkeys from "react-hot-keys"; | ||||
| import { useSwipeable } from "react-swipeable"; | ||||
| import { Img } from "./Img"; | ||||
| import { Button } from "./Inputs/Button"; | ||||
| import { Popup } from "./Popup"; | ||||
| 
 | ||||
| interface Props { | ||||
|   setState: | ||||
| @ -12,27 +16,75 @@ interface Props { | ||||
|   setIndex: Dispatch<SetStateAction<number>>; | ||||
| } | ||||
| 
 | ||||
| export default function LightBox(props: Props): JSX.Element { | ||||
| export function LightBox(props: Immutable<Props>): JSX.Element { | ||||
|   const { state, setState, images, index, setIndex } = props; | ||||
|   const mobile = useMediaMobile(); | ||||
| 
 | ||||
|   function handlePrevious() { | ||||
|     if (index > 0) setIndex(index - 1); | ||||
|   } | ||||
| 
 | ||||
|   function handleNext() { | ||||
|     if (index < images.length - 1) setIndex(index + 1); | ||||
|   } | ||||
| 
 | ||||
|   const sensibilitySwipe = 0.5; | ||||
| 
 | ||||
|   const handlers = useSwipeable({ | ||||
|     onSwipedLeft: (SwipeEventData) => { | ||||
|       if (SwipeEventData.velocity < sensibilitySwipe) return; | ||||
|       handleNext(); | ||||
|     }, | ||||
|     onSwipedRight: (SwipeEventData) => { | ||||
|       if (SwipeEventData.velocity < sensibilitySwipe) return; | ||||
|       handlePrevious(); | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {state && ( | ||||
|         <Lightbox | ||||
|           reactModalProps={{ | ||||
|             parentSelector: () => document.getElementById("MyAppLayout"), | ||||
|         <Hotkeys | ||||
|           keyName="left,right" | ||||
|           allowRepeat | ||||
|           onKeyDown={(keyName) => { | ||||
|             if (keyName === "left") { | ||||
|               handlePrevious(); | ||||
|             } else { | ||||
|               handleNext(); | ||||
|             } | ||||
|           }} | ||||
|           mainSrc={images[index]} | ||||
|           prevSrc={index > 0 ? images[index - 1] : undefined} | ||||
|           nextSrc={index < images.length ? images[index + 1] : undefined} | ||||
|           onMovePrevRequest={() => setIndex(index - 1)} | ||||
|           onMoveNextRequest={() => setIndex(index + 1)} | ||||
|           imageCaption="" | ||||
|           imageTitle="" | ||||
|           onCloseRequest={() => setState(false)} | ||||
|           imagePadding={mobile ? 0 : 70} | ||||
|         /> | ||||
|         > | ||||
|           <Popup setState={setState} state={state} padding={false} fillViewport> | ||||
|             <div | ||||
|               {...handlers} | ||||
|               className={`grid grid-cols-[4em,1fr,4em] mobile:grid-cols-2
 | ||||
|               [grid-template-areas:"left_image_right"] | ||||
|               mobile:[grid-template-areas:"image_image""left_right"] | ||||
|               place-items-center first-letter:gap-4 w-full h-full overflow-hidden`}
 | ||||
|             > | ||||
|               <div className="[grid-area:left]"> | ||||
|                 {index > 0 && ( | ||||
|                   <Button onClick={handlePrevious}> | ||||
|                     <span className="material-icons">chevron_left</span> | ||||
|                   </Button> | ||||
|                 )} | ||||
|               </div> | ||||
| 
 | ||||
|               <Img | ||||
|                 className="max-h-full [grid-area:image]" | ||||
|                 image={images[index]} | ||||
|               /> | ||||
| 
 | ||||
|               <div className="[grid-area:right]"> | ||||
|                 {index < images.length - 1 && ( | ||||
|                   <Button onClick={handleNext}> | ||||
|                     <span className="material-icons">chevron_right</span> | ||||
|                   </Button> | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           </Popup> | ||||
|         </Hotkeys> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| import HorizontalLine from "components/HorizontalLine"; | ||||
| import Img, { getAssetURL, ImageQuality } from "components/Img"; | ||||
| import InsetBox from "components/InsetBox"; | ||||
| import LightBox from "components/LightBox"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Img } from "components/Img"; | ||||
| import { InsetBox } from "components/InsetBox"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { slugify } from "helpers/formatters"; | ||||
| import { getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useLightBox } from "hooks/useLightBox"; | ||||
| import Markdown from "markdown-to-jsx"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { slugify } from "queries/helpers"; | ||||
| import React, { useState } from "react"; | ||||
| import React from "react"; | ||||
| import ReactDOMServer from "react-dom/server"; | ||||
| 
 | ||||
| interface Props { | ||||
| @ -15,26 +17,18 @@ interface Props { | ||||
|   text: string; | ||||
| } | ||||
| 
 | ||||
| export default function Markdawn(props: Props): JSX.Element { | ||||
| export function Markdawn(props: Immutable<Props>): JSX.Element { | ||||
|   const appLayout = useAppLayout(); | ||||
|   const text = preprocessMarkDawn(props.text); | ||||
| 
 | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   const [lightboxOpen, setLightboxOpen] = useState(false); | ||||
|   const [lightboxImages, setLightboxImages] = useState([""]); | ||||
|   const [lightboxIndex, setLightboxIndex] = useState(0); | ||||
|   const [openLightBox, LightBox] = useLightBox(); | ||||
| 
 | ||||
|   if (text) { | ||||
|     return ( | ||||
|       <> | ||||
|         <LightBox | ||||
|           state={lightboxOpen} | ||||
|           setState={setLightboxOpen} | ||||
|           images={lightboxImages} | ||||
|           index={lightboxIndex} | ||||
|           setIndex={setLightboxIndex} | ||||
|         /> | ||||
|         <LightBox /> | ||||
|         <Markdown | ||||
|           className={`formatted ${props.className}`} | ||||
|           options={{ | ||||
| @ -253,13 +247,11 @@ export default function Markdawn(props: Props): JSX.Element { | ||||
|                   <div | ||||
|                     className="my-8 cursor-pointer place-content-center grid" | ||||
|                     onClick={() => { | ||||
|                       setLightboxOpen(true); | ||||
|                       setLightboxImages([ | ||||
|                       openLightBox([ | ||||
|                         compProps.src.startsWith("/uploads/") | ||||
|                           ? getAssetURL(compProps.src, ImageQuality.Large) | ||||
|                           : compProps.src, | ||||
|                       ]); | ||||
|                       setLightboxIndex(0); | ||||
|                     }} | ||||
|                   > | ||||
|                     <Img | ||||
| @ -268,8 +260,6 @@ export default function Markdawn(props: Props): JSX.Element { | ||||
|                           ? getAssetURL(compProps.src, ImageQuality.Small) | ||||
|                           : compProps.src | ||||
|                       } | ||||
|                       layout="fill" | ||||
|                       objectFit="contain" | ||||
|                       quality={ImageQuality.Medium} | ||||
|                     ></Img> | ||||
|                   </div> | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { slugify } from "helpers/formatters"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { slugify } from "queries/helpers"; | ||||
| import { preprocessMarkDawn } from "./Markdawn"; | ||||
| 
 | ||||
| interface Props { | ||||
| @ -7,7 +8,7 @@ interface Props { | ||||
|   title?: string; | ||||
| } | ||||
| 
 | ||||
| export default function TOCComponent(props: Props): JSX.Element { | ||||
| export function TOC(props: Immutable<Props>): JSX.Element { | ||||
|   const { text, title } = props; | ||||
|   const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title); | ||||
|   const router = useRouter(); | ||||
| @ -28,7 +29,7 @@ export default function TOCComponent(props: Props): JSX.Element { | ||||
| } | ||||
| 
 | ||||
| interface LevelProps { | ||||
|   tocchildren: TOC[]; | ||||
|   tocchildren: TOCInterface[]; | ||||
|   parentNumbering: string; | ||||
| } | ||||
| 
 | ||||
| @ -60,14 +61,14 @@ function TOCLevel(props: LevelProps): JSX.Element { | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| interface TOC { | ||||
| interface TOCInterface { | ||||
|   title: string; | ||||
|   slug: string; | ||||
|   children: TOC[]; | ||||
|   children: TOCInterface[]; | ||||
| } | ||||
| 
 | ||||
| export function getTocFromMarkdawn(text: string, title?: string): TOC { | ||||
|   const toc: TOC = { | ||||
| export function getTocFromMarkdawn(text: string, title?: string): TOCInterface { | ||||
|   const toc: TOCInterface = { | ||||
|     title: title ?? "Return to top", | ||||
|     slug: slugify(title), | ||||
|     children: [], | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { MouseEventHandler } from "react"; | ||||
| 
 | ||||
| @ -12,15 +13,19 @@ interface Props { | ||||
|   onClick?: MouseEventHandler<HTMLDivElement>; | ||||
| } | ||||
| 
 | ||||
| export default function NavOption(props: Props): JSX.Element { | ||||
| export function NavOption(props: Immutable<Props>): JSX.Element { | ||||
|   const router = useRouter(); | ||||
|   const isActive = router.asPath.startsWith(props.url); | ||||
|   const divActive = "bg-mid shadow-inner-sm shadow-shade"; | ||||
| 
 | ||||
|   const border = | ||||
|     "outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent]"; | ||||
|   const divCommon = `gap-x-5 w-full rounded-2xl cursor-pointer p-4 hover:bg-mid hover:shadow-inner-sm hover:shadow-shade hover:active:shadow-inner hover:active:shadow-shade transition-all ${ | ||||
|     props.border ? border : "" | ||||
|   } ${isActive ? divActive : ""}`;
 | ||||
| 
 | ||||
|   const divCommon = `gap-x-5 w-full rounded-2xl cursor-pointer p-4 hover:bg-mid
 | ||||
|   hover:shadow-inner-sm hover:shadow-shade hover:active:shadow-inner | ||||
|   hover:active:shadow-shade transition-all ${props.border ? border : ""} ${ | ||||
|     isActive ? divActive : "" | ||||
|   }`;
 | ||||
| 
 | ||||
|   return ( | ||||
|     <ToolTip | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import HorizontalLine from "components/HorizontalLine"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   icon?: string; | ||||
| @ -6,7 +7,7 @@ interface Props { | ||||
|   description?: string | null | undefined; | ||||
| } | ||||
| 
 | ||||
| export default function PanelHeader(props: Props): JSX.Element { | ||||
| export function PanelHeader(props: Immutable<Props>): JSX.Element { | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="w-full grid place-items-center"> | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| import HorizontalLine from "components/HorizontalLine"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   href: string; | ||||
| @ -18,7 +19,7 @@ export enum ReturnButtonType { | ||||
|   both = "both", | ||||
| } | ||||
| 
 | ||||
| export default function ReturnButton(props: Props): JSX.Element { | ||||
| export function ReturnButton(props: Immutable<Props>): JSX.Element { | ||||
|   const appLayout = useAppLayout(); | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   children: React.ReactNode; | ||||
|   autoformat?: boolean; | ||||
| @ -9,13 +11,13 @@ export enum ContentPanelWidthSizes { | ||||
|   large = "large", | ||||
| } | ||||
| 
 | ||||
| export default function ContentPanel(props: Props): JSX.Element { | ||||
| export function ContentPanel(props: Immutable<Props>): JSX.Element { | ||||
|   const width = props.width ? props.width : ContentPanelWidthSizes.default; | ||||
|   const widthCSS = | ||||
|     width === ContentPanelWidthSizes.default ? "max-w-2xl" : "w-full"; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={`grid pt-10 pb-20 px-6 desktop:py-20 desktop:px-10`}> | ||||
|     <div className={`grid pt-10 pb-20 px-4 desktop:py-20 desktop:px-10`}> | ||||
|       <main | ||||
|         className={`${ | ||||
|           props.autoformat && "formatted" | ||||
|  | ||||
| @ -1,25 +1,27 @@ | ||||
| import HorizontalLine from "components/HorizontalLine"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import NavOption from "components/PanelComponents/NavOption"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useMediaDesktop } from "hooks/useMediaQuery"; | ||||
| import Markdown from "markdown-to-jsx"; | ||||
| import Link from "next/link"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props { | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| export default function MainPanel(props: Props): JSX.Element { | ||||
| export function MainPanel(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const isDesktop = useMediaDesktop(); | ||||
|   const appLayout = useAppLayout(); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       className={`flex flex-col justify-center content-start gap-y-2 justify-items-center text-center p-8 ${ | ||||
|       className={`flex flex-col justify-center content-start
 | ||||
|       gap-y-2 justify-items-center text-center p-8 ${ | ||||
|         appLayout.mainPanelReduced && isDesktop && "px-4" | ||||
|       }`}
 | ||||
|     > | ||||
| @ -44,7 +46,9 @@ export default function MainPanel(props: Props): JSX.Element { | ||||
|               onClick={() => appLayout.setMainPanelOpen(false)} | ||||
|               className={`${ | ||||
|                 appLayout.mainPanelReduced && isDesktop ? "w-12" : "w-1/2" | ||||
|               } aspect-square cursor-pointer transition-colors [mask:url('/icons/accords.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black hover:bg-dark mb-4`}
 | ||||
|               } aspect-square cursor-pointer transition-colors [mask:url('/icons/accords.svg')] | ||||
|               ![mask-size:contain] ![mask-repeat:no-repeat] | ||||
|               ![mask-position:center] bg-black hover:bg-dark mb-4`}
 | ||||
|             ></div> | ||||
|           </Link> | ||||
| 
 | ||||
| @ -68,22 +72,11 @@ export default function MainPanel(props: Props): JSX.Element { | ||||
|               disabled={!appLayout.mainPanelReduced} | ||||
|             > | ||||
|               <Button | ||||
|                 className={ | ||||
|                   appLayout.mainPanelReduced && isDesktop | ||||
|                     ? "" | ||||
|                     : "!py-0.5 !px-2.5" | ||||
|                 } | ||||
|                 onClick={() => { | ||||
|                   appLayout.setConfigPanelOpen(true); | ||||
|                 }} | ||||
|               > | ||||
|                 <span | ||||
|                   className={`material-icons ${ | ||||
|                     !(appLayout.mainPanelReduced && isDesktop) && "!text-sm" | ||||
|                   } `}
 | ||||
|                 > | ||||
|                   settings | ||||
|                 </span> | ||||
|                 <span className={"material-icons"}>settings</span> | ||||
|               </Button> | ||||
|             </ToolTip> | ||||
| 
 | ||||
| @ -218,10 +211,22 @@ export default function MainPanel(props: Props): JSX.Element { | ||||
|           className="transition-[filter] colorize-black hover:colorize-dark" | ||||
|           href="https://creativecommons.org/licenses/by-sa/4.0/" | ||||
|         > | ||||
|           <div className="mt-4 mb-8 grid grid-flow-col place-content-center gap-1 hover:[--theme-color-black:var(--theme-color-dark)]"> | ||||
|             <div className="w-6 aspect-square [mask:url('/icons/creative-commons-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" /> | ||||
|             <div className="w-6 aspect-square [mask:url('/icons/creative-commons-by-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" /> | ||||
|             <div className="w-6 aspect-square [mask:url('/icons/creative-commons-sa-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" /> | ||||
|           <div | ||||
|             className="mt-4 mb-8 grid grid-flow-col place-content-center gap-1 | ||||
|             hover:[--theme-color-black:var(--theme-color-dark)]" | ||||
|           > | ||||
|             <div | ||||
|               className="w-6 aspect-square [mask:url('/icons/creative-commons-brands.svg')] | ||||
|               ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" | ||||
|             /> | ||||
|             <div | ||||
|               className="w-6 aspect-square [mask:url('/icons/creative-commons-by-brands.svg')] | ||||
|               ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" | ||||
|             /> | ||||
|             <div | ||||
|               className="w-6 aspect-square [mask:url('/icons/creative-commons-sa-brands.svg')] | ||||
|               ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" | ||||
|             /> | ||||
|           </div> | ||||
|         </a> | ||||
|         <p> | ||||
| @ -232,14 +237,18 @@ export default function MainPanel(props: Props): JSX.Element { | ||||
|         <div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8"> | ||||
|           <a | ||||
|             aria-label="Browse our GitHub repository, which include this website source code" | ||||
|             className="transition-colors [mask:url('/icons/github-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] w-10 aspect-square bg-black hover:bg-dark" | ||||
|             className="transition-colors [mask:url('/icons/github-brands.svg')] | ||||
|             ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] | ||||
|             w-10 aspect-square bg-black hover:bg-dark" | ||||
|             href="https://github.com/Accords-Library" | ||||
|             target="_blank" | ||||
|             rel="noopener noreferrer" | ||||
|           ></a> | ||||
|           <a | ||||
|             aria-label="Join our Discord server!" | ||||
|             className="transition-colors [mask:url('/icons/discord-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] w-10 aspect-square bg-black hover:bg-dark" | ||||
|             className="transition-colors [mask:url('/icons/discord-brands.svg')] | ||||
|             ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] | ||||
|             w-10 aspect-square bg-black hover:bg-dark" | ||||
|             href="/discord" | ||||
|             target="_blank" | ||||
|             rel="noopener noreferrer" | ||||
|  | ||||
| @ -1,8 +1,10 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   children: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| export default function SubPanel(props: Props): JSX.Element { | ||||
| export function SubPanel(props: Immutable<Props>): JSX.Element { | ||||
|   return ( | ||||
|     <div className="grid pt-10 pb-20 px-6 desktop:py-8 desktop:px-10 gap-y-2 text-center"> | ||||
|       {props.children} | ||||
|  | ||||
| @ -1,4 +1,7 @@ | ||||
| import { Dispatch, SetStateAction } from "react"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { Dispatch, SetStateAction, useEffect } from "react"; | ||||
| import Hotkeys from "react-hot-keys"; | ||||
| 
 | ||||
| interface Props { | ||||
|   setState: | ||||
| @ -8,42 +11,65 @@ interface Props { | ||||
|   children: React.ReactNode; | ||||
|   fillViewport?: boolean; | ||||
|   hideBackground?: boolean; | ||||
|   padding?: boolean; | ||||
| } | ||||
| 
 | ||||
| export default function Popup(props: Props): JSX.Element { | ||||
| export function Popup(props: Immutable<Props>): JSX.Element { | ||||
|   const { | ||||
|     setState, | ||||
|     state, | ||||
|     children, | ||||
|     fillViewport, | ||||
|     hideBackground, | ||||
|     padding = true, | ||||
|   } = props; | ||||
| 
 | ||||
|   const appLayout = useAppLayout(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     appLayout.setMenuGestures(!state); | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [state]); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       className={`fixed inset-0 z-50 grid place-content-center
 | ||||
|       transition-[backdrop-filter] duration-500 ${ | ||||
|         props.state | ||||
|           ? "[backdrop-filter:blur(2px)]" | ||||
|           : "pointer-events-none touch-none" | ||||
|       }`}
 | ||||
|     <Hotkeys | ||||
|       keyName="escape" | ||||
|       allowRepeat | ||||
|       onKeyDown={() => { | ||||
|         setState(false); | ||||
|       }} | ||||
|     > | ||||
|       <div | ||||
|         className={`fixed bg-shade inset-0 transition-all duration-500 ${ | ||||
|           props.state ? "bg-opacity-50" : "bg-opacity-0" | ||||
|         }`}
 | ||||
|         onClick={() => { | ||||
|           props.setState(false); | ||||
|         }} | ||||
|       /> | ||||
| 
 | ||||
|       <div | ||||
|         className={`p-10 grid gap-4 place-items-center transition-transform ${ | ||||
|           props.state ? "scale-100" : "scale-0" | ||||
|         } ${ | ||||
|           props.fillViewport | ||||
|             ? "absolute inset-10 top-20" | ||||
|             : "relative max-h-[80vh] overflow-y-auto mobile:w-[85vw]" | ||||
|         } ${ | ||||
|           props.hideBackground | ||||
|             ? "" | ||||
|             : "bg-light rounded-lg shadow-2xl shadow-shade" | ||||
|         }`}
 | ||||
|         className={`fixed inset-0 z-50 grid place-content-center
 | ||||
|       transition-[backdrop-filter] duration-500 ${ | ||||
|         state ? "[backdrop-filter:blur(2px)]" : "pointer-events-none touch-none" | ||||
|       }`}
 | ||||
|       > | ||||
|         {props.children} | ||||
|         <div | ||||
|           className={`fixed bg-shade inset-0 transition-all duration-500 ${ | ||||
|             state ? "bg-opacity-50" : "bg-opacity-0" | ||||
|           }`}
 | ||||
|           onClick={() => { | ||||
|             setState(false); | ||||
|           }} | ||||
|         /> | ||||
| 
 | ||||
|         <div | ||||
|           className={`${ | ||||
|             padding && "p-10 mobile:p-6" | ||||
|           } grid gap-4 place-items-center transition-transform ${ | ||||
|             state ? "scale-100" : "scale-0" | ||||
|           } ${ | ||||
|             fillViewport | ||||
|               ? "absolute inset-10" | ||||
|               : "relative max-h-[80vh] overflow-y-auto mobile:w-[85vw]" | ||||
|           } ${ | ||||
|             hideBackground ? "" : "bg-light rounded-lg shadow-2xl shadow-shade" | ||||
|           }`}
 | ||||
|         > | ||||
|           {children} | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     </Hotkeys> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -1,29 +1,22 @@ | ||||
| import { GetPostQuery } from "graphql/generated"; | ||||
| import useSmartLanguage from "hooks/useSmartLanguage"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { getStatusDescription, prettySlug } from "queries/helpers"; | ||||
| import AppLayout from "./AppLayout"; | ||||
| import Chip from "./Chip"; | ||||
| import HorizontalLine from "./HorizontalLine"; | ||||
| import Markdawn from "./Markdown/Markdawn"; | ||||
| import TOC from "./Markdown/TOC"; | ||||
| import ReturnButton, { ReturnButtonType } from "./PanelComponents/ReturnButton"; | ||||
| import ContentPanel from "./Panels/ContentPanel"; | ||||
| import SubPanel from "./Panels/SubPanel"; | ||||
| import RecorderChip from "./RecorderChip"; | ||||
| import ThumbnailHeader from "./ThumbnailHeader"; | ||||
| import ToolTip from "./ToolTip"; | ||||
| 
 | ||||
| export type Post = Exclude< | ||||
|   Exclude< | ||||
|     GetPostQuery["posts"], | ||||
|     null | undefined | ||||
|   >["data"][number]["attributes"], | ||||
|   null | undefined | ||||
| >; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { getStatusDescription } from "helpers/others"; | ||||
| import { Immutable, PostWithTranslations } from "helpers/types"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { AppLayout } from "./AppLayout"; | ||||
| import { Chip } from "./Chip"; | ||||
| import { HorizontalLine } from "./HorizontalLine"; | ||||
| import { Markdawn } from "./Markdown/Markdawn"; | ||||
| import { TOC } from "./Markdown/TOC"; | ||||
| import { ReturnButton, ReturnButtonType } from "./PanelComponents/ReturnButton"; | ||||
| import { ContentPanel } from "./Panels/ContentPanel"; | ||||
| import { SubPanel } from "./Panels/SubPanel"; | ||||
| import { RecorderChip } from "./RecorderChip"; | ||||
| import { ThumbnailHeader } from "./ThumbnailHeader"; | ||||
| import { ToolTip } from "./ToolTip"; | ||||
| 
 | ||||
| interface Props { | ||||
|   post: Post; | ||||
|   post: PostWithTranslations; | ||||
|   langui: AppStaticProps["langui"]; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   currencies: AppStaticProps["currencies"]; | ||||
| @ -38,7 +31,7 @@ interface Props { | ||||
|   appendBody?: JSX.Element; | ||||
| } | ||||
| 
 | ||||
| export default function PostPage(props: Props): JSX.Element { | ||||
| export function PostPage(props: Immutable<Props>): JSX.Element { | ||||
|   const { | ||||
|     post, | ||||
|     langui, | ||||
|  | ||||
| @ -4,16 +4,18 @@ import { | ||||
|   PricePickerFragment, | ||||
|   UploadImageFragment, | ||||
| } from "graphql/generated"; | ||||
| import Link from "next/link"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { | ||||
|   prettyDate, | ||||
|   prettyDuration, | ||||
|   prettyPrice, | ||||
|   prettyShortenNumber, | ||||
| } from "queries/helpers"; | ||||
| import Chip from "./Chip"; | ||||
| import Img, { ImageQuality } from "./Img"; | ||||
| } from "helpers/formatters"; | ||||
| import { ImageQuality } from "helpers/img"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import Link from "next/link"; | ||||
| import { Chip } from "./Chip"; | ||||
| import { Img } from "./Img"; | ||||
| 
 | ||||
| interface Props { | ||||
|   thumbnail?: UploadImageFragment | string | null | undefined; | ||||
| @ -26,6 +28,7 @@ interface Props { | ||||
|   topChips?: string[]; | ||||
|   bottomChips?: string[]; | ||||
|   keepInfoVisible?: boolean; | ||||
|   stackNumber?: number; | ||||
|   metadata?: { | ||||
|     currencies?: AppStaticProps["currencies"]; | ||||
|     release_date?: DatePickerFragment | null; | ||||
| @ -42,7 +45,7 @@ interface Props { | ||||
|     | { __typename: "anotherHoverlayName" }; | ||||
| } | ||||
| 
 | ||||
| export default function ThumbnailPreview(props: Props): JSX.Element { | ||||
| export function PreviewCard(props: Immutable<Props>): JSX.Element { | ||||
|   const { | ||||
|     href, | ||||
|     thumbnail, | ||||
| @ -50,6 +53,7 @@ export default function ThumbnailPreview(props: Props): JSX.Element { | ||||
|     title, | ||||
|     subtitle, | ||||
|     description, | ||||
|     stackNumber = 0, | ||||
|     topChips, | ||||
|     bottomChips, | ||||
|     keepInfoVisible, | ||||
| @ -110,8 +114,41 @@ export default function ThumbnailPreview(props: Props): JSX.Element { | ||||
|         className="drop-shadow-shade-xl cursor-pointer grid items-end | ||||
|         fine:[--cover-opacity:0] hover:[--cover-opacity:1] hover:scale-[1.02] | ||||
|         [--bg-opacity:0] hover:[--bg-opacity:0.5] [--play-opacity:0] | ||||
|         hover:[--play-opacity:100] transition-transform" | ||||
|         hover:[--play-opacity:100] transition-transform | ||||
|         [--stacked-top:0] hover:[--stacked-top:1]" | ||||
|       > | ||||
|         {stackNumber > 0 && ( | ||||
|           <> | ||||
|             <div | ||||
|               className="bg-light rounded-md overflow-hidden absolute transition-[top_transform] | ||||
|               inset-0 -top-[var(--stacked-top)*2.1rem] brightness-[0.8] sepia-[0.5] | ||||
|               scale-[calc(1-0.15*var(--stacked-top))]" | ||||
|             > | ||||
|               {thumbnail && ( | ||||
|                 <Img | ||||
|                   className="opacity-30 " | ||||
|                   image={thumbnail} | ||||
|                   quality={ImageQuality.Medium} | ||||
|                 /> | ||||
|               )} | ||||
|             </div> | ||||
| 
 | ||||
|             <div | ||||
|               className="bg-light rounded-md overflow-hidden absolute transition-[top_transform] | ||||
|               -top-[var(--stacked-top)*1rem] inset-0 brightness-[0.9] sepia-[0.2] | ||||
|               scale-[calc(1-0.06*var(--stacked-top))]" | ||||
|             > | ||||
|               {thumbnail && ( | ||||
|                 <Img | ||||
|                   className="opacity-70" | ||||
|                   image={thumbnail} | ||||
|                   quality={ImageQuality.Medium} | ||||
|                 /> | ||||
|               )} | ||||
|             </div> | ||||
|           </> | ||||
|         )} | ||||
| 
 | ||||
|         {thumbnail ? ( | ||||
|           <div className="relative"> | ||||
|             <Img | ||||
| @ -123,6 +160,14 @@ export default function ThumbnailPreview(props: Props): JSX.Element { | ||||
|               image={thumbnail} | ||||
|               quality={ImageQuality.Medium} | ||||
|             /> | ||||
|             {stackNumber > 0 && ( | ||||
|               <div | ||||
|                 className="absolute right-2 top-2 text-light bg-black | ||||
|                   bg-opacity-60 px-2 rounded-full" | ||||
|               > | ||||
|                 {stackNumber} | ||||
|               </div> | ||||
|             )} | ||||
|             {hoverlay && hoverlay.__typename === "Video" && ( | ||||
|               <> | ||||
|                 <div | ||||
| @ -149,18 +194,26 @@ export default function ThumbnailPreview(props: Props): JSX.Element { | ||||
|         ) : ( | ||||
|           <div | ||||
|             style={{ aspectRatio: thumbnailAspectRatio }} | ||||
|             className={`w-full bg-light ${ | ||||
|             className={`w-full bg-light relative ${ | ||||
|               keepInfoVisible | ||||
|                 ? "rounded-t-md" | ||||
|                 : "rounded-md coarse:rounded-b-none" | ||||
|             }`}
 | ||||
|           ></div> | ||||
|           > | ||||
|             {stackNumber > 0 && ( | ||||
|               <div | ||||
|                 className="absolute right-2 top-2 text-light bg-black | ||||
|                 bg-opacity-60 px-2 rounded-full" | ||||
|               > | ||||
|                 {stackNumber} | ||||
|               </div> | ||||
|             )} | ||||
|           </div> | ||||
|         )} | ||||
|         <div | ||||
|           className={`linearbg-obi ${ | ||||
|             keepInfoVisible | ||||
|               ? "-mt-[0.3333em]" | ||||
|               : `fine:drop-shadow-shade-lg fine:absolute coarse:rounded-b-md
 | ||||
|             !keepInfoVisible && | ||||
|             `fine:drop-shadow-shade-lg fine:absolute coarse:rounded-b-md
 | ||||
|               bottom-2 -inset-x-0.5 opacity-[var(--cover-opacity)]` | ||||
|           } transition-opacity z-20 grid p-4 gap-2`}
 | ||||
|         > | ||||
| @ -173,11 +226,15 @@ export default function ThumbnailPreview(props: Props): JSX.Element { | ||||
|             </div> | ||||
|           )} | ||||
|           <div className="my-1"> | ||||
|             {pre_title && <p className="leading-none mb-1">{pre_title}</p>} | ||||
|             {title && ( | ||||
|               <p className="font-headers text-lg leading-none">{title}</p> | ||||
|             {pre_title && ( | ||||
|               <p className="leading-none mb-1 break-words">{pre_title}</p> | ||||
|             )} | ||||
|             {subtitle && <p className="leading-none">{subtitle}</p>} | ||||
|             {title && ( | ||||
|               <p className="font-headers text-lg leading-none break-words"> | ||||
|                 {title} | ||||
|               </p> | ||||
|             )} | ||||
|             {subtitle && <p className="leading-none break-words">{subtitle}</p>} | ||||
|           </div> | ||||
|           {description && <p>{description}</p>} | ||||
|           {bottomChips && bottomChips.length > 0 && ( | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| import { UploadImageFragment } from "graphql/generated"; | ||||
| import { ImageQuality } from "helpers/img"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import Link from "next/link"; | ||||
| import Chip from "./Chip"; | ||||
| import Img, { ImageQuality } from "./Img"; | ||||
| import { Chip } from "./Chip"; | ||||
| import { Img } from "./Img"; | ||||
| 
 | ||||
| interface Props { | ||||
|   thumbnail?: UploadImageFragment | string | null | undefined; | ||||
| @ -14,7 +16,7 @@ interface Props { | ||||
|   bottomChips?: string[]; | ||||
| } | ||||
| 
 | ||||
| export default function PreviewLine(props: Props): JSX.Element { | ||||
| export function PreviewLine(props: Immutable<Props>): JSX.Element { | ||||
|   const { | ||||
|     href, | ||||
|     thumbnail, | ||||
| @ -29,8 +31,9 @@ export default function PreviewLine(props: Props): JSX.Element { | ||||
|   return ( | ||||
|     <Link href={href} passHref> | ||||
|       <div | ||||
|         className="drop-shadow-shade-xl rounded-md bg-light cursor-pointer hover:scale-[1.02] | ||||
|          transition-transform flex flex-row gap-4 overflow-hidden place-items-center pr-4 w-full h-36" | ||||
|         className="drop-shadow-shade-xl rounded-md bg-light cursor-pointer | ||||
|         hover:scale-[1.02] transition-transform flex flex-row gap-4 | ||||
|         overflow-hidden place-items-center pr-4 w-full h-36" | ||||
|       > | ||||
|         {thumbnail ? ( | ||||
|           <div className="h-full aspect-[3/2]"> | ||||
|  | ||||
| @ -1,9 +1,11 @@ | ||||
| import Chip from "components/Chip"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { RecorderChipFragment } from "graphql/generated"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import Img, { ImageQuality } from "./Img"; | ||||
| import Markdawn from "./Markdown/Markdawn"; | ||||
| import ToolTip from "./ToolTip"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { ImageQuality } from "helpers/img"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { Img } from "./Img"; | ||||
| import { Markdawn } from "./Markdown/Markdawn"; | ||||
| import { ToolTip } from "./ToolTip"; | ||||
| 
 | ||||
| interface Props { | ||||
|   className?: string; | ||||
| @ -11,7 +13,7 @@ interface Props { | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| export default function RecorderChip(props: Props): JSX.Element { | ||||
| export function RecorderChip(props: Immutable<Props>): JSX.Element { | ||||
|   const { recorder, langui } = props; | ||||
|   return ( | ||||
|     <ToolTip | ||||
| @ -49,10 +51,7 @@ export default function RecorderChip(props: Props): JSX.Element { | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           {recorder.bio?.[0] && <Markdawn text={recorder.bio[0].bio ?? ""} />} | ||||
| 
 | ||||
|           {/* <Button className="cursor-not-allowed">View profile</Button> */} | ||||
|         </div> | ||||
|       } | ||||
|       placement="top" | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { Immutable } from "helpers/types"; | ||||
| import Image from "next/image"; | ||||
| 
 | ||||
| interface Props { | ||||
| @ -6,7 +7,7 @@ interface Props { | ||||
|   className?: string; | ||||
| } | ||||
| 
 | ||||
| export default function SVG(props: Props): JSX.Element { | ||||
| export function SVG(props: Immutable<Props>): JSX.Element { | ||||
|   return ( | ||||
|     <div className={props.className}> | ||||
|       <Image | ||||
|  | ||||
| @ -1,36 +1,31 @@ | ||||
| import Chip from "components/Chip"; | ||||
| import Img, { ImageQuality } from "components/Img"; | ||||
| import InsetBox from "components/InsetBox"; | ||||
| import Markdawn from "components/Markdown/Markdawn"; | ||||
| import { GetContentQuery, UploadImageFragment } from "graphql/generated"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { prettyinlineTitle, prettySlug, slugify } from "queries/helpers"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Img } from "components/Img"; | ||||
| import { InsetBox } from "components/InsetBox"; | ||||
| import { Markdawn } from "components/Markdown/Markdawn"; | ||||
| import { GetContentTextQuery, UploadImageFragment } from "graphql/generated"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { prettyinlineTitle, prettySlug, slugify } from "helpers/formatters"; | ||||
| import { getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useLightBox } from "hooks/useLightBox"; | ||||
| 
 | ||||
| interface Props { | ||||
|   pre_title?: string | null | undefined; | ||||
|   title: string | null | undefined; | ||||
|   subtitle?: string | null | undefined; | ||||
|   description?: string | null | undefined; | ||||
|   type?: Exclude< | ||||
|     Exclude< | ||||
|       GetContentQuery["contents"], | ||||
|       null | undefined | ||||
|     >["data"][number]["attributes"], | ||||
|     null | undefined | ||||
|   type?: NonNullable< | ||||
|     NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"] | ||||
|   >["type"]; | ||||
|   categories?: Exclude< | ||||
|     Exclude< | ||||
|       GetContentQuery["contents"], | ||||
|       null | undefined | ||||
|     >["data"][number]["attributes"], | ||||
|     null | undefined | ||||
|   categories?: NonNullable< | ||||
|     NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"] | ||||
|   >["categories"]; | ||||
|   thumbnail?: UploadImageFragment | null | undefined; | ||||
|   langui: AppStaticProps["langui"]; | ||||
|   languageSwitcher?: JSX.Element; | ||||
| } | ||||
| 
 | ||||
| export default function ThumbnailHeader(props: Props): JSX.Element { | ||||
| export function ThumbnailHeader(props: Immutable<Props>): JSX.Element { | ||||
|   const { | ||||
|     langui, | ||||
|     pre_title, | ||||
| @ -43,16 +38,21 @@ export default function ThumbnailHeader(props: Props): JSX.Element { | ||||
|     languageSwitcher, | ||||
|   } = props; | ||||
| 
 | ||||
|   const [openLightBox, LightBox] = useLightBox(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <LightBox /> | ||||
|       <div className="grid place-items-center gap-12 mb-12"> | ||||
|         <div className="drop-shadow-shade-lg"> | ||||
|           {thumbnail ? ( | ||||
|             <Img | ||||
|               className=" rounded-xl" | ||||
|               className="rounded-xl cursor-pointer" | ||||
|               image={thumbnail} | ||||
|               quality={ImageQuality.Medium} | ||||
|               priority | ||||
|               onClick={() => { | ||||
|                 openLightBox([getAssetURL(thumbnail.url, ImageQuality.Large)]); | ||||
|               }} | ||||
|             /> | ||||
|           ) : ( | ||||
|             <div className="w-96 aspect-[4/3] bg-light rounded-xl"></div> | ||||
|  | ||||
| @ -3,13 +3,13 @@ import "tippy.js/animations/scale-subtle.css"; | ||||
| 
 | ||||
| interface Props extends TippyProps {} | ||||
| 
 | ||||
| export default function ToolTip(props: Props): JSX.Element { | ||||
|   const newProps = { ...props }; | ||||
| 
 | ||||
|   // Set defaults
 | ||||
|   if (newProps.delay === undefined) newProps.delay = [150, 0]; | ||||
|   if (newProps.interactive === undefined) newProps.interactive = true; | ||||
|   if (newProps.animation === undefined) newProps.animation = "scale-subtle"; | ||||
| export function ToolTip(props: Props): JSX.Element { | ||||
|   const newProps: Props = { | ||||
|     delay: [150, 0], | ||||
|     interactive: true, | ||||
|     animation: "scale-subtle", | ||||
|     ...props, | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Tippy className={`text-[80%] ${newProps.className}`} {...newProps}> | ||||
|  | ||||
| @ -1,22 +1,20 @@ | ||||
| import Chip from "components/Chip"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { | ||||
|   Enum_Componenttranslationschronologyitem_Status, | ||||
|   GetChronologyItemsQuery, | ||||
| } from "graphql/generated"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { getStatusDescription } from "queries/helpers"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getStatusDescription } from "helpers/others"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   item: Exclude< | ||||
|     GetChronologyItemsQuery["chronologyItems"], | ||||
|     null | undefined | ||||
|   >["data"][number]; | ||||
|   item: NonNullable<GetChronologyItemsQuery["chronologyItems"]>["data"][number]; | ||||
|   displayYear: boolean; | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| export default function ChronologyItemComponent(props: Props): JSX.Element { | ||||
| export function ChronologyItemComponent(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
| 
 | ||||
|   function generateAnchor( | ||||
| @ -71,7 +69,8 @@ export default function ChronologyItemComponent(props: Props): JSX.Element { | ||||
|   if (props.item.attributes) { | ||||
|     return ( | ||||
|       <div | ||||
|         className="grid place-content-start grid-rows-[auto_1fr] grid-cols-[4em] py-4 px-8 rounded-2xl target:bg-mid target:py-8 target:my-4" | ||||
|         className="grid place-content-start grid-rows-[auto_1fr] grid-cols-[4em] | ||||
|         py-4 px-8 rounded-2xl target:bg-mid target:py-8 target:my-4" | ||||
|         id={generateAnchor( | ||||
|           props.item.attributes.year, | ||||
|           props.item.attributes.month, | ||||
| @ -100,7 +99,10 @@ export default function ChronologyItemComponent(props: Props): JSX.Element { | ||||
|                     <> | ||||
|                       {translation && ( | ||||
|                         <> | ||||
|                           <div className="place-items-start place-content-start grid grid-flow-col gap-2"> | ||||
|                           <div | ||||
|                             className="place-items-start | ||||
|                             place-content-start grid grid-flow-col gap-2" | ||||
|                           > | ||||
|                             {translation.status !== | ||||
|                               Enum_Componenttranslationschronologyitem_Status.Done && ( | ||||
|                               <ToolTip | ||||
| @ -125,7 +127,8 @@ export default function ChronologyItemComponent(props: Props): JSX.Element { | ||||
|                               className={ | ||||
|                                 event.translations && | ||||
|                                 event.translations.length > 1 | ||||
|                                   ? "before:content-['-'] before:text-dark before:inline-block before:w-4 before:ml-[-1em] mt-2 whitespace-pre-line" | ||||
|                                   ? `before:content-['-'] before:text-dark before:inline-block
 | ||||
|                                     before:w-4 before:ml-[-1em] mt-2 whitespace-pre-line` | ||||
|                                   : "whitespace-pre-line" | ||||
|                               } | ||||
|                             > | ||||
|  | ||||
| @ -1,17 +1,17 @@ | ||||
| import ChronologyItemComponent from "components/Wiki/Chronology/ChronologyItemComponent"; | ||||
| import { ChronologyItemComponent } from "components/Wiki/Chronology/ChronologyItemComponent"; | ||||
| import { GetChronologyItemsQuery } from "graphql/generated"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props { | ||||
|   year: number; | ||||
|   items: Exclude< | ||||
|     GetChronologyItemsQuery["chronologyItems"], | ||||
|     null | undefined | ||||
|   items: NonNullable< | ||||
|     GetChronologyItemsQuery["chronologyItems"] | ||||
|   >["data"][number][]; | ||||
|   langui: AppStaticProps["langui"]; | ||||
| } | ||||
| 
 | ||||
| export default function ChronologyYearComponent(props: Props): JSX.Element { | ||||
| export function ChronologyYearComponent(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
| 
 | ||||
|   return ( | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import useDarkMode from "hooks/useDarkMode"; | ||||
| import useStateWithLocalStorage from "hooks/useStateWithLocalStorage"; | ||||
| import React, { ReactNode, useContext } from "react"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useDarkMode } from "hooks/useDarkMode"; | ||||
| import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage"; | ||||
| import React, { ReactNode, useContext, useState } from "react"; | ||||
| 
 | ||||
| interface AppLayoutState { | ||||
|   subPanelOpen: boolean | undefined; | ||||
| @ -14,6 +15,7 @@ interface AppLayoutState { | ||||
|   currency: string | undefined; | ||||
|   playerName: string | undefined; | ||||
|   preferredLanguages: string[] | undefined; | ||||
|   menuGestures: boolean; | ||||
|   setSubPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>; | ||||
|   setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>; | ||||
|   setMainPanelReduced: React.Dispatch< | ||||
| @ -31,6 +33,7 @@ interface AppLayoutState { | ||||
|   setPreferredLanguages: React.Dispatch< | ||||
|     React.SetStateAction<string[] | undefined> | ||||
|   >; | ||||
|   setMenuGestures: React.Dispatch<React.SetStateAction<boolean>>; | ||||
| } | ||||
| 
 | ||||
| /* eslint-disable @typescript-eslint/no-empty-function */ | ||||
| @ -46,6 +49,7 @@ const initialState: AppLayoutState = { | ||||
|   currency: "USD", | ||||
|   playerName: "", | ||||
|   preferredLanguages: [], | ||||
|   menuGestures: true, | ||||
|   setSubPanelOpen: () => {}, | ||||
|   setMainPanelReduced: () => {}, | ||||
|   setMainPanelOpen: () => {}, | ||||
| @ -57,6 +61,7 @@ const initialState: AppLayoutState = { | ||||
|   setCurrency: () => {}, | ||||
|   setPlayerName: () => {}, | ||||
|   setPreferredLanguages: () => {}, | ||||
|   setMenuGestures: () => {}, | ||||
| }; | ||||
| /* eslint-enable @typescript-eslint/no-empty-function */ | ||||
| 
 | ||||
| @ -72,7 +77,7 @@ interface Props { | ||||
|   children: ReactNode; | ||||
| } | ||||
| 
 | ||||
| export function AppContextProvider(props: Props): JSX.Element { | ||||
| export function AppContextProvider(props: Immutable<Props>): JSX.Element { | ||||
|   const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage< | ||||
|     boolean | undefined | ||||
|   >("subPanelOpen", initialState.subPanelOpen); | ||||
| @ -115,6 +120,8 @@ export function AppContextProvider(props: Props): JSX.Element { | ||||
|     string[] | undefined | ||||
|   >("preferredLanguages", initialState.preferredLanguages); | ||||
| 
 | ||||
|   const [menuGestures, setMenuGestures] = useState(false); | ||||
| 
 | ||||
|   return ( | ||||
|     <AppContext.Provider | ||||
|       value={{ | ||||
| @ -129,6 +136,7 @@ export function AppContextProvider(props: Props): JSX.Element { | ||||
|         currency, | ||||
|         playerName, | ||||
|         preferredLanguages, | ||||
|         menuGestures, | ||||
|         setSubPanelOpen, | ||||
|         setConfigPanelOpen, | ||||
|         setMainPanelReduced, | ||||
| @ -140,6 +148,7 @@ export function AppContextProvider(props: Props): JSX.Element { | ||||
|         setCurrency, | ||||
|         setPlayerName, | ||||
|         setPreferredLanguages, | ||||
|         setMenuGestures, | ||||
|       }} | ||||
|     > | ||||
|       {props.children} | ||||
|  | ||||
| @ -4,22 +4,18 @@ import { | ||||
|   GetWebsiteInterfaceQuery, | ||||
| } from "graphql/generated"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| 
 | ||||
| export interface AppStaticProps { | ||||
|   langui: Exclude< | ||||
|     Exclude< | ||||
|       GetWebsiteInterfaceQuery["websiteInterfaces"], | ||||
|       null | undefined | ||||
|     >["data"][number]["attributes"], | ||||
|     null | undefined | ||||
| export type AppStaticProps = Immutable<{ | ||||
|   langui: NonNullable< | ||||
|     NonNullable< | ||||
|       GetWebsiteInterfaceQuery["websiteInterfaces"] | ||||
|     >["data"][number]["attributes"] | ||||
|   >; | ||||
|   currencies: Exclude< | ||||
|     GetCurrenciesQuery["currencies"], | ||||
|     null | undefined | ||||
|   >["data"]; | ||||
|   languages: Exclude<GetLanguagesQuery["languages"], null | undefined>["data"]; | ||||
| } | ||||
|   currencies: NonNullable<GetCurrenciesQuery["currencies"]>["data"]; | ||||
|   languages: NonNullable<GetLanguagesQuery["languages"]>["data"]; | ||||
| }>; | ||||
| 
 | ||||
| export async function getAppStaticProps( | ||||
|   context: GetStaticPropsContext | ||||
| @ -50,9 +46,11 @@ export async function getAppStaticProps( | ||||
|     }) | ||||
|   ).websiteInterfaces?.data[0].attributes; | ||||
| 
 | ||||
|   return { | ||||
|     langui: langui ?? ({} as AppStaticProps["langui"]), | ||||
|     currencies: currencies?.data ?? ({} as AppStaticProps["currencies"]), | ||||
|     languages: languages?.data ?? ({} as AppStaticProps["languages"]), | ||||
|   const appStaticProps: AppStaticProps = { | ||||
|     langui: langui ?? {}, | ||||
|     currencies: currencies?.data ?? [], | ||||
|     languages: languages?.data ?? [], | ||||
|   }; | ||||
| 
 | ||||
|   return appStaticProps; | ||||
| } | ||||
							
								
								
									
										32
									
								
								src/graphql/getPostStaticProps.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/graphql/getPostStaticProps.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| import { PostWithTranslations } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "./getAppStaticProps"; | ||||
| import { getReadySdk } from "./sdk"; | ||||
| 
 | ||||
| export interface PostStaticProps extends AppStaticProps { | ||||
|   post: PostWithTranslations; | ||||
| } | ||||
| 
 | ||||
| export function getPostStaticProps( | ||||
|   slug: string | ||||
| ): ( | ||||
|   context: GetStaticPropsContext | ||||
| ) => Promise<{ notFound: boolean } | { props: PostStaticProps }> { | ||||
|   return async (context: GetStaticPropsContext) => { | ||||
|     const sdk = getReadySdk(); | ||||
|     const post = await sdk.getPost({ | ||||
|       slug: slug, | ||||
|       language_code: context.locale ?? "en", | ||||
|     }); | ||||
|     if (post.posts?.data[0].attributes?.translations) { | ||||
|       const props: PostStaticProps = { | ||||
|         ...(await getAppStaticProps(context)), | ||||
|         post: post.posts.data[0].attributes as PostWithTranslations, | ||||
|       }; | ||||
|       return { | ||||
|         props: props, | ||||
|       }; | ||||
|     } | ||||
|     return { notFound: true }; | ||||
|   }; | ||||
| } | ||||
| @ -14,7 +14,13 @@ query devGetContents { | ||||
|             id | ||||
|           } | ||||
|         } | ||||
|         titles { | ||||
| 
 | ||||
|         ranged_contents { | ||||
|           data { | ||||
|             id | ||||
|           } | ||||
|         } | ||||
|         translations { | ||||
|           language { | ||||
|             data { | ||||
|               id | ||||
| @ -22,62 +28,36 @@ query devGetContents { | ||||
|           } | ||||
|           title | ||||
|           description | ||||
|         } | ||||
|         ranged_contents { | ||||
|           data { | ||||
|             id | ||||
|           } | ||||
|         } | ||||
|         text_set { | ||||
|           language { | ||||
|             data { | ||||
|               id | ||||
|           text_set { | ||||
|             source_language { | ||||
|               data { | ||||
|                 id | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           source_language { | ||||
|             data { | ||||
|               id | ||||
|             status | ||||
|             transcribers { | ||||
|               data { | ||||
|                 id | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           status | ||||
|           transcribers { | ||||
|             data { | ||||
|               id | ||||
|             translators { | ||||
|               data { | ||||
|                 id | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           translators { | ||||
|             data { | ||||
|               id | ||||
|             proofreaders { | ||||
|               data { | ||||
|                 id | ||||
|               } | ||||
|             } | ||||
|             text | ||||
|           } | ||||
|           proofreaders { | ||||
|             data { | ||||
|               id | ||||
|             } | ||||
|           } | ||||
|           text | ||||
|         } | ||||
|         video_set { | ||||
|           id | ||||
|         } | ||||
|         audio_set { | ||||
|           id | ||||
|         } | ||||
|         thumbnail { | ||||
|           data { | ||||
|             id | ||||
|           } | ||||
|         } | ||||
|         next_recommended { | ||||
|           data { | ||||
|             id | ||||
|           } | ||||
|         } | ||||
|         previous_recommended { | ||||
|           data { | ||||
|             id | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -1,77 +0,0 @@ | ||||
| query getContent($slug: String, $language_code: String) { | ||||
|   contents(filters: { slug: { eq: $slug } }) { | ||||
|     data { | ||||
|       attributes { | ||||
|         slug | ||||
|         titles(filters: { language: { code: { eq: $language_code } } }) { | ||||
|           pre_title | ||||
|           title | ||||
|           subtitle | ||||
|           description | ||||
|         } | ||||
|         categories { | ||||
|           data { | ||||
|             id | ||||
|             attributes { | ||||
|               name | ||||
|               short | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         type { | ||||
|           data { | ||||
|             attributes { | ||||
|               slug | ||||
|               titles(filters: { language: { code: { eq: $language_code } } }) { | ||||
|                 title | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         ranged_contents { | ||||
|           data { | ||||
|             id | ||||
|             attributes { | ||||
|               slug | ||||
|               scan_set { | ||||
|                 id | ||||
|               } | ||||
|               library_item { | ||||
|                 data { | ||||
|                   attributes { | ||||
|                     slug | ||||
|                     title | ||||
|                     subtitle | ||||
|                     thumbnail { | ||||
|                       data { | ||||
|                         attributes { | ||||
|                           ...uploadImage | ||||
|                         } | ||||
|                       } | ||||
|                     } | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         text_set { | ||||
|           id | ||||
|         } | ||||
|         video_set { | ||||
|           id | ||||
|         } | ||||
|         audio_set { | ||||
|           id | ||||
|         } | ||||
|         thumbnail { | ||||
|           data { | ||||
|             attributes { | ||||
|               ...uploadImage | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -4,12 +4,7 @@ query getContentText($slug: String, $language_code: String) { | ||||
|       id | ||||
|       attributes { | ||||
|         slug | ||||
|         titles { | ||||
|           pre_title | ||||
|           title | ||||
|           subtitle | ||||
|           description | ||||
|         } | ||||
| 
 | ||||
|         categories { | ||||
|           data { | ||||
|             id | ||||
| @ -56,9 +51,7 @@ query getContentText($slug: String, $language_code: String) { | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         text_set { | ||||
|           status | ||||
|           text | ||||
|         translations { | ||||
|           language { | ||||
|             data { | ||||
|               attributes { | ||||
| @ -66,39 +59,48 @@ query getContentText($slug: String, $language_code: String) { | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           source_language { | ||||
|             data { | ||||
|               attributes { | ||||
|                 code | ||||
|           pre_title | ||||
|           title | ||||
|           subtitle | ||||
|           description | ||||
|           text_set { | ||||
|             status | ||||
|             text | ||||
|             source_language { | ||||
|               data { | ||||
|                 attributes { | ||||
|                   code | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           transcribers { | ||||
|             data { | ||||
|               id | ||||
|               attributes { | ||||
|                 ...recorderChip | ||||
|             transcribers { | ||||
|               data { | ||||
|                 id | ||||
|                 attributes { | ||||
|                   ...recorderChip | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           translators { | ||||
|             data { | ||||
|               id | ||||
|               attributes { | ||||
|                 ...recorderChip | ||||
|             translators { | ||||
|               data { | ||||
|                 id | ||||
|                 attributes { | ||||
|                   ...recorderChip | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           proofreaders { | ||||
|             data { | ||||
|               id | ||||
|               attributes { | ||||
|                 ...recorderChip | ||||
|             proofreaders { | ||||
|               data { | ||||
|                 id | ||||
|                 attributes { | ||||
|                   ...recorderChip | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|             notes | ||||
|           } | ||||
|           notes | ||||
|         } | ||||
| 
 | ||||
|         thumbnail { | ||||
|           data { | ||||
|             attributes { | ||||
| @ -106,78 +108,47 @@ query getContentText($slug: String, $language_code: String) { | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         previous_recommended { | ||||
|         group { | ||||
|           data { | ||||
|             attributes { | ||||
|               slug | ||||
|               titles(filters: { language: { code: { eq: $language_code } } }) { | ||||
|                 pre_title | ||||
|                 title | ||||
|                 subtitle | ||||
|               } | ||||
|               categories { | ||||
|                 data { | ||||
|                   id | ||||
|                   attributes { | ||||
|                     short | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|               type { | ||||
|               contents { | ||||
|                 data { | ||||
|                   attributes { | ||||
|                     slug | ||||
|                     titles( | ||||
|                       filters: { language: { code: { eq: $language_code } } } | ||||
|                     ) { | ||||
|                     translations { | ||||
|                       pre_title | ||||
|                       title | ||||
|                       subtitle | ||||
|                     } | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|               thumbnail { | ||||
|                 data { | ||||
|                   attributes { | ||||
|                     ...uploadImage | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         next_recommended { | ||||
|           data { | ||||
|             attributes { | ||||
|               slug | ||||
|               titles(filters: { language: { code: { eq: $language_code } } }) { | ||||
|                 pre_title | ||||
|                 title | ||||
|                 subtitle | ||||
|               } | ||||
|               categories { | ||||
|                 data { | ||||
|                   id | ||||
|                   attributes { | ||||
|                     short | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|               type { | ||||
|                 data { | ||||
|                   attributes { | ||||
|                     slug | ||||
|                     titles( | ||||
|                       filters: { language: { code: { eq: $language_code } } } | ||||
|                     ) { | ||||
|                       title | ||||
|                     categories { | ||||
|                       data { | ||||
|                         id | ||||
|                         attributes { | ||||
|                           short | ||||
|                         } | ||||
|                       } | ||||
|                     } | ||||
|                     type { | ||||
|                       data { | ||||
|                         attributes { | ||||
|                           slug | ||||
|                           titles( | ||||
|                             filters: { | ||||
|                               language: { code: { eq: $language_code } } | ||||
|                             } | ||||
|                           ) { | ||||
|                             title | ||||
|                           } | ||||
|                         } | ||||
|                       } | ||||
|                     } | ||||
|                     thumbnail { | ||||
|                       data { | ||||
|                         attributes { | ||||
|                           ...uploadImage | ||||
|                         } | ||||
|                       } | ||||
|                     } | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|               thumbnail { | ||||
|                 data { | ||||
|                   attributes { | ||||
|                     ...uploadImage | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|  | ||||
| @ -4,7 +4,7 @@ query getContents($language_code: String) { | ||||
|       id | ||||
|       attributes { | ||||
|         slug | ||||
|         titles(filters: { language: { code: { eq: $language_code } } }) { | ||||
|         translations { | ||||
|           pre_title | ||||
|           title | ||||
|           subtitle | ||||
| @ -55,14 +55,17 @@ query getContents($language_code: String) { | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         text_set { | ||||
|           id | ||||
|         } | ||||
|         video_set { | ||||
|           id | ||||
|         } | ||||
|         audio_set { | ||||
|           id | ||||
|         group { | ||||
|           data { | ||||
|             attributes { | ||||
|               combine | ||||
|               contents { | ||||
|                 data { | ||||
|                   id | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         thumbnail { | ||||
|           data { | ||||
|  | ||||
| @ -362,22 +362,18 @@ query getLibraryItem($slug: String, $language_code: String) { | ||||
|                         } | ||||
|                       } | ||||
|                     } | ||||
|                     titles( | ||||
|                       filters: { language: { code: { eq: $language_code } } } | ||||
|                     ) { | ||||
|                     translations { | ||||
|                       language { | ||||
|                         data { | ||||
|                           attributes { | ||||
|                             code | ||||
|                           } | ||||
|                         } | ||||
|                       } | ||||
|                       pre_title | ||||
|                       title | ||||
|                       subtitle | ||||
|                     } | ||||
|                     text_set { | ||||
|                       id | ||||
|                     } | ||||
|                     video_set { | ||||
|                       id | ||||
|                     } | ||||
|                     audio_set { | ||||
|                       id | ||||
|                     } | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|  | ||||
| @ -132,6 +132,19 @@ query getWebsiteInterface($language_code: String) { | ||||
|         response_invalid_code | ||||
|         response_invalid_email | ||||
|         response_email_success | ||||
|         always_show_info | ||||
|         item_not_available | ||||
|         primary_language | ||||
|         secondary_language | ||||
|         combine_related_contents | ||||
|         previous_content | ||||
|         followup_content | ||||
|         videos | ||||
|         view_on | ||||
|         channel | ||||
|         subscribers | ||||
|         description | ||||
|         available_at | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
							
								
								
									
										31
									
								
								src/helpers/contents.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/helpers/contents.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| import { ContentWithTranslations, Immutable } from "./types"; | ||||
| 
 | ||||
| type Group = Immutable< | ||||
|   NonNullable< | ||||
|     NonNullable< | ||||
|       NonNullable< | ||||
|         NonNullable<ContentWithTranslations["group"]>["data"] | ||||
|       >["attributes"] | ||||
|     >["contents"] | ||||
|   >["data"] | ||||
| >; | ||||
| 
 | ||||
| export function getPreviousContent(group: Group, currentSlug: string) { | ||||
|   for (let index = 0; index < group.length; index += 1) { | ||||
|     const content = group[index]; | ||||
|     if (content.attributes?.slug === currentSlug && index > 0) { | ||||
|       return group[index - 1]; | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
| 
 | ||||
| export function getNextContent(group: Group, currentSlug: string) { | ||||
|   for (let index = 0; index < group.length; index += 1) { | ||||
|     const content = group[index]; | ||||
|     if (content.attributes?.slug === currentSlug && index < group.length - 1) { | ||||
|       return group[index + 1]; | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
| @ -1,20 +1,9 @@ | ||||
| import { | ||||
|   getAssetURL, | ||||
|   getImgSizesByQuality, | ||||
|   ImageQuality, | ||||
| } from "components/Img"; | ||||
| import { | ||||
|   DatePickerFragment, | ||||
|   Enum_Componentsetstextset_Status, | ||||
|   GetCurrenciesQuery, | ||||
|   GetLibraryItemQuery, | ||||
|   GetLibraryItemScansQuery, | ||||
|   PricePickerFragment, | ||||
|   UploadImageFragment, | ||||
| } from "graphql/generated"; | ||||
| import { AppStaticProps } from "./getAppStaticProps"; | ||||
| import { DatePickerFragment, PricePickerFragment } from "graphql/generated"; | ||||
| import { AppStaticProps } from "../graphql/getAppStaticProps"; | ||||
| import { convertPrice } from "./numbers"; | ||||
| import { Immutable } from "./types"; | ||||
| 
 | ||||
| export function prettyDate(datePicker: DatePickerFragment): string { | ||||
| export function prettyDate(datePicker: Immutable<DatePickerFragment>): string { | ||||
|   let result = ""; | ||||
|   if (datePicker.year) result += datePicker.year.toString(); | ||||
|   if (datePicker.month) | ||||
| @ -25,7 +14,7 @@ export function prettyDate(datePicker: DatePickerFragment): string { | ||||
| } | ||||
| 
 | ||||
| export function prettyPrice( | ||||
|   pricePicker: PricePickerFragment, | ||||
|   pricePicker: Immutable<PricePickerFragment>, | ||||
|   currencies: AppStaticProps["currencies"], | ||||
|   targetCurrencyCode?: string | ||||
| ): string { | ||||
| @ -45,25 +34,6 @@ export function prettyPrice( | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| export function convertPrice( | ||||
|   pricePicker: PricePickerFragment, | ||||
|   targetCurrency: Exclude< | ||||
|     GetCurrenciesQuery["currencies"], | ||||
|     null | undefined | ||||
|   >["data"][number] | ||||
| ): number { | ||||
|   if ( | ||||
|     pricePicker.amount && | ||||
|     pricePicker.currency?.data?.attributes && | ||||
|     targetCurrency.attributes | ||||
|   ) | ||||
|     return ( | ||||
|       (pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) / | ||||
|       targetCurrency.attributes.rate_to_usd | ||||
|     ); | ||||
|   return 0; | ||||
| } | ||||
| 
 | ||||
| export function prettySlug(slug?: string, parentSlug?: string): string { | ||||
|   if (slug) { | ||||
|     if (parentSlug && slug.startsWith(parentSlug)) | ||||
| @ -88,7 +58,7 @@ export function prettyinlineTitle( | ||||
| } | ||||
| 
 | ||||
| export function prettyItemType( | ||||
|   metadata: any, | ||||
|   metadata: Immutable<any>, | ||||
|   langui: AppStaticProps["langui"] | ||||
| ): string | undefined | null { | ||||
|   switch (metadata.__typename) { | ||||
| @ -110,7 +80,7 @@ export function prettyItemType( | ||||
| } | ||||
| 
 | ||||
| export function prettyItemSubType( | ||||
|   metadata: | ||||
|   metadata: Immutable< | ||||
|     | { | ||||
|         __typename: "ComponentMetadataAudio"; | ||||
|         subtype?: { | ||||
| @ -187,6 +157,7 @@ export function prettyItemSubType( | ||||
|       } | ||||
|     | { __typename: "Error" } | ||||
|     | null | ||||
|   > | ||||
| ): string { | ||||
|   if (metadata) { | ||||
|     switch (metadata.__typename) { | ||||
| @ -300,87 +271,6 @@ export function capitalizeString(string: string): string { | ||||
|   return words.join(" "); | ||||
| } | ||||
| 
 | ||||
| export function convertMmToInch(mm: number | null | undefined): string { | ||||
|   return mm ? (mm * 0.03937008).toPrecision(3) : ""; | ||||
| } | ||||
| 
 | ||||
| export interface OgImage { | ||||
|   image: string; | ||||
|   width: number; | ||||
|   height: number; | ||||
|   alt: string; | ||||
| } | ||||
| 
 | ||||
| export function getOgImage( | ||||
|   quality: ImageQuality, | ||||
|   image: UploadImageFragment | ||||
| ): OgImage { | ||||
|   const imgSize = getImgSizesByQuality( | ||||
|     image.width ?? 0, | ||||
|     image.height ?? 0, | ||||
|     quality ? quality : ImageQuality.Small | ||||
|   ); | ||||
|   return { | ||||
|     image: getAssetURL(image.url, quality), | ||||
|     width: imgSize.width, | ||||
|     height: imgSize.height, | ||||
|     alt: image.alternativeText || "", | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function sortContent( | ||||
|   contents: | ||||
|     | Exclude< | ||||
|         Exclude< | ||||
|           GetLibraryItemQuery["libraryItems"], | ||||
|           null | undefined | ||||
|         >["data"][number]["attributes"], | ||||
|         null | undefined | ||||
|       >["contents"] | ||||
|     | Exclude< | ||||
|         Exclude< | ||||
|           GetLibraryItemScansQuery["libraryItems"], | ||||
|           null | undefined | ||||
|         >["data"][number]["attributes"], | ||||
|         null | undefined | ||||
|       >["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( | ||||
|   status: string, | ||||
|   langui: AppStaticProps["langui"] | ||||
| ): string | null | undefined { | ||||
|   switch (status) { | ||||
|     case Enum_Componentsetstextset_Status.Incomplete: | ||||
|       return langui.status_incomplete; | ||||
| 
 | ||||
|     case Enum_Componentsetstextset_Status.Draft: | ||||
|       return langui.status_draft; | ||||
| 
 | ||||
|     case Enum_Componentsetstextset_Status.Review: | ||||
|       return langui.status_review; | ||||
| 
 | ||||
|     case Enum_Componentsetstextset_Status.Done: | ||||
|       return langui.status_done; | ||||
| 
 | ||||
|     default: | ||||
|       return ""; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function slugify(string: string | undefined): string { | ||||
|   if (!string) { | ||||
|     return ""; | ||||
| @ -400,51 +290,3 @@ export function slugify(string: string | undefined): string { | ||||
|     .trim() | ||||
|     .replace(/ /gu, "-"); | ||||
| } | ||||
| 
 | ||||
| export function randomInt(min: number, max: number) { | ||||
|   return Math.floor(Math.random() * (max - min)) + min; | ||||
| } | ||||
| 
 | ||||
| export function getLocalesFromLanguages( | ||||
|   languages?: Array<{ | ||||
|     language?: { | ||||
|       data?: { | ||||
|         attributes?: { code: string } | null; | ||||
|       } | null; | ||||
|     } | null; | ||||
|   } | null> | null | ||||
| ) { | ||||
|   return languages | ||||
|     ? languages.map((language) => language?.language?.data?.attributes?.code) | ||||
|     : []; | ||||
| } | ||||
| 
 | ||||
| export function getVideoThumbnailURL(uid: string): string { | ||||
|   return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.webp`; | ||||
| } | ||||
| 
 | ||||
| export function getVideoFile(uid: string): string { | ||||
|   return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.mp4`; | ||||
| } | ||||
| 
 | ||||
| export function arrayMove<T>(arr: T[], old_index: number, new_index: number) { | ||||
|   arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); | ||||
|   return arr; | ||||
| } | ||||
| 
 | ||||
| export function getPreferredLanguage( | ||||
|   preferredLanguages: (string | undefined)[], | ||||
|   availableLanguages: Map<string, number> | ||||
| ): number | undefined { | ||||
|   for (const locale of preferredLanguages) { | ||||
|     if (locale && availableLanguages.has(locale)) { | ||||
|       return availableLanguages.get(locale); | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
| 
 | ||||
| export function isInteger(value: string): boolean { | ||||
|   // eslint-disable-next-line require-unicode-regexp
 | ||||
|   return /^[+-]?[0-9]+$/.test(value); | ||||
| } | ||||
							
								
								
									
										88
									
								
								src/helpers/img.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/helpers/img.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | ||||
| import { UploadImageFragment } from "graphql/generated"; | ||||
| import { Immutable } from "./types"; | ||||
| 
 | ||||
| export enum ImageQuality { | ||||
|   Small = "small", | ||||
|   Medium = "medium", | ||||
|   Large = "large", | ||||
|   Og = "og", | ||||
| } | ||||
| 
 | ||||
| export interface OgImage { | ||||
|   image: string; | ||||
|   width: number; | ||||
|   height: number; | ||||
|   alt: string; | ||||
| } | ||||
| 
 | ||||
| export function getAssetFilename(path: string): string { | ||||
|   let result = path.split("/"); | ||||
|   result = result[result.length - 1].split("."); | ||||
|   result = result | ||||
|     .splice(0, result.length - 1) | ||||
|     .join(".") | ||||
|     .split("_"); | ||||
|   return result[0]; | ||||
| } | ||||
| 
 | ||||
| export function getAssetURL( | ||||
|   url: string, | ||||
|   quality: Immutable<ImageQuality> | ||||
| ): string { | ||||
|   let newUrl = url; | ||||
|   newUrl = newUrl.replace(/^\/uploads/u, `/${quality}`); | ||||
|   newUrl = newUrl.replace(/.jpg$/u, ".webp"); | ||||
|   newUrl = newUrl.replace(/.jpeg$/u, ".webp"); | ||||
|   newUrl = newUrl.replace(/.png$/u, ".webp"); | ||||
|   if (quality === ImageQuality.Og) newUrl = newUrl.replace(/.webp$/u, ".jpg"); | ||||
|   return process.env.NEXT_PUBLIC_URL_IMG + newUrl; | ||||
| } | ||||
| 
 | ||||
| export function getImgSizesByMaxSize( | ||||
|   width: number, | ||||
|   height: number, | ||||
|   maxSize: number | ||||
| ): { width: number; height: number } { | ||||
|   if (width > height) { | ||||
|     if (width < maxSize) return { width: width, height: height }; | ||||
|     return { width: maxSize, height: (height / width) * maxSize }; | ||||
|   } | ||||
|   if (height < maxSize) return { width: width, height: height }; | ||||
|   return { width: (width / height) * maxSize, height: maxSize }; | ||||
| } | ||||
| 
 | ||||
| export function getImgSizesByQuality( | ||||
|   width: number, | ||||
|   height: number, | ||||
|   quality: ImageQuality | ||||
| ): { width: number; height: number } { | ||||
|   switch (quality) { | ||||
|     case ImageQuality.Og: | ||||
|       return getImgSizesByMaxSize(width, height, 512); | ||||
|     case ImageQuality.Small: | ||||
|       return getImgSizesByMaxSize(width, height, 512); | ||||
|     case ImageQuality.Medium: | ||||
|       return getImgSizesByMaxSize(width, height, 1024); | ||||
|     case ImageQuality.Large: | ||||
|       return getImgSizesByMaxSize(width, height, 2048); | ||||
|     default: | ||||
|       return { width: 0, height: 0 }; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function getOgImage( | ||||
|   quality: Immutable<ImageQuality>, | ||||
|   image: Immutable<UploadImageFragment> | ||||
| ): OgImage { | ||||
|   const imgSize = getImgSizesByQuality( | ||||
|     image.width ?? 0, | ||||
|     image.height ?? 0, | ||||
|     quality ? quality : ImageQuality.Small | ||||
|   ); | ||||
|   return { | ||||
|     image: getAssetURL(image.url, quality), | ||||
|     width: imgSize.width, | ||||
|     height: imgSize.height, | ||||
|     alt: image.alternativeText || "", | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										33
									
								
								src/helpers/numbers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/helpers/numbers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| import { GetCurrenciesQuery, PricePickerFragment } from "graphql/generated"; | ||||
| import { Immutable } from "./types"; | ||||
| 
 | ||||
| export function convertPrice( | ||||
|   pricePicker: Immutable<PricePickerFragment>, | ||||
|   targetCurrency: Immutable< | ||||
|     NonNullable<GetCurrenciesQuery["currencies"]>["data"][number] | ||||
|   > | ||||
| ): number { | ||||
|   if ( | ||||
|     pricePicker.amount && | ||||
|     pricePicker.currency?.data?.attributes && | ||||
|     targetCurrency.attributes | ||||
|   ) | ||||
|     return ( | ||||
|       (pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) / | ||||
|       targetCurrency.attributes.rate_to_usd | ||||
|     ); | ||||
|   return 0; | ||||
| } | ||||
| 
 | ||||
| export function convertMmToInch(mm: number | null | undefined): string { | ||||
|   return mm ? (mm * 0.03937008).toPrecision(3) : ""; | ||||
| } | ||||
| 
 | ||||
| export function randomInt(min: number, max: number) { | ||||
|   return Math.floor(Math.random() * (max - min)) + min; | ||||
| } | ||||
| 
 | ||||
| export function isInteger(value: string): boolean { | ||||
|   // eslint-disable-next-line require-unicode-regexp
 | ||||
|   return /^[+-]?[0-9]+$/.test(value); | ||||
| } | ||||
							
								
								
									
										66
									
								
								src/helpers/others.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/helpers/others.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| import { | ||||
|   Enum_Componentsetstextset_Status, | ||||
|   GetLibraryItemQuery, | ||||
|   GetLibraryItemScansQuery, | ||||
| } from "graphql/generated"; | ||||
| import { AppStaticProps } from "../graphql/getAppStaticProps"; | ||||
| import { Immutable } from "./types"; | ||||
| 
 | ||||
| type SortContentProps = | ||||
|   | NonNullable< | ||||
|       NonNullable< | ||||
|         GetLibraryItemQuery["libraryItems"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["contents"] | ||||
|   | NonNullable< | ||||
|       NonNullable< | ||||
|         GetLibraryItemScansQuery["libraryItems"] | ||||
|       >["data"][number]["attributes"] | ||||
|     >["contents"]; | ||||
| 
 | ||||
| export function sortContent(contents: Immutable<SortContentProps>) { | ||||
|   if (contents) { | ||||
|     const newContent = { ...contents } as SortContentProps; | ||||
|     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 as Immutable<SortContentProps>; | ||||
|   } | ||||
|   return contents; | ||||
| } | ||||
| 
 | ||||
| export function getStatusDescription( | ||||
|   status: string, | ||||
|   langui: AppStaticProps["langui"] | ||||
| ): string | null | undefined { | ||||
|   switch (status) { | ||||
|     case Enum_Componentsetstextset_Status.Incomplete: | ||||
|       return langui.status_incomplete; | ||||
| 
 | ||||
|     case Enum_Componentsetstextset_Status.Draft: | ||||
|       return langui.status_draft; | ||||
| 
 | ||||
|     case Enum_Componentsetstextset_Status.Review: | ||||
|       return langui.status_review; | ||||
| 
 | ||||
|     case Enum_Componentsetstextset_Status.Done: | ||||
|       return langui.status_done; | ||||
| 
 | ||||
|     default: | ||||
|       return ""; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function arrayMove<T>(arr: T[], old_index: number, new_index: number) { | ||||
|   arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); | ||||
|   return arr; | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/helpers/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/helpers/types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| import { GetContentTextQuery, GetPostQuery } from "graphql/generated"; | ||||
| import React from "react"; | ||||
| 
 | ||||
| type Post = NonNullable< | ||||
|   NonNullable<GetPostQuery["posts"]>["data"][number]["attributes"] | ||||
| >; | ||||
| 
 | ||||
| export interface PostWithTranslations extends Omit<Post, "translations"> { | ||||
|   translations: NonNullable<Post["translations"]>; | ||||
| } | ||||
| 
 | ||||
| type Content = NonNullable< | ||||
|   NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"] | ||||
| >; | ||||
| 
 | ||||
| export interface ContentWithTranslations extends Omit<Content, "translations"> { | ||||
|   translations: NonNullable<Content["translations"]>; | ||||
| } | ||||
| 
 | ||||
| type ImmutableBlackList<T> = JSX.Element | React.ReactNode | Function; | ||||
| 
 | ||||
| export type Immutable<T> = { | ||||
|   readonly [K in keyof T]: T[K] extends ImmutableBlackList<T> | ||||
|     ? T[K] | ||||
|     : Immutable<T[K]>; | ||||
| }; | ||||
							
								
								
									
										7
									
								
								src/helpers/videos.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/helpers/videos.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| export function getVideoThumbnailURL(uid: string): string { | ||||
|   return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.webp`; | ||||
| } | ||||
| 
 | ||||
| export function getVideoFile(uid: string): string { | ||||
|   return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.mp4`; | ||||
| } | ||||
| @ -1,8 +1,8 @@ | ||||
| import { useEffect } from "react"; | ||||
| import { usePrefersDarkMode } from "./useMediaQuery"; | ||||
| import useStateWithLocalStorage from "./useStateWithLocalStorage"; | ||||
| import { useStateWithLocalStorage } from "./useStateWithLocalStorage"; | ||||
| 
 | ||||
| export default function useDarkMode( | ||||
| export function useDarkMode( | ||||
|   key: string, | ||||
|   initialValue: boolean | undefined | ||||
| ): [ | ||||
|  | ||||
							
								
								
									
										28
									
								
								src/hooks/useLightBox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/hooks/useLightBox.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| import { LightBox } from "components/LightBox"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| export function useLightBox(): [ | ||||
|   (images: string[], index?: number) => void, | ||||
|   () => JSX.Element | ||||
| ] { | ||||
|   const [lightboxOpen, setLightboxOpen] = useState(false); | ||||
|   const [lightboxImages, setLightboxImages] = useState([""]); | ||||
|   const [lightboxIndex, setLightboxIndex] = useState(0); | ||||
| 
 | ||||
|   return [ | ||||
|     (images: string[], index = 0) => { | ||||
|       setLightboxOpen(true); | ||||
|       setLightboxImages(images); | ||||
|       setLightboxIndex(index); | ||||
|     }, | ||||
|     () => ( | ||||
|       <LightBox | ||||
|         state={lightboxOpen} | ||||
|         setState={setLightboxOpen} | ||||
|         images={lightboxImages} | ||||
|         index={lightboxIndex} | ||||
|         setIndex={setLightboxIndex} | ||||
|       /> | ||||
|     ), | ||||
|   ]; | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| export default function useMediaQuery(query: string): boolean { | ||||
| export function useMediaQuery(query: string): boolean { | ||||
|   function getMatches(query: string): boolean { | ||||
|     // Prevents SSR issues
 | ||||
|     if (typeof window !== "undefined") { | ||||
|  | ||||
| @ -1,20 +1,32 @@ | ||||
| import LanguageSwitcher from "components/Inputs/LanguageSwitcher"; | ||||
| import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { AppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { getPreferredLanguage } from "queries/helpers"; | ||||
| import { useEffect, useMemo, useState } from "react"; | ||||
| 
 | ||||
| interface Props<T> { | ||||
|   items: T[]; | ||||
|   items: Immutable<T[]>; | ||||
|   languages: AppStaticProps["languages"]; | ||||
|   languageExtractor: (item: T) => string | undefined; | ||||
|   transform?: (item: T) => T; | ||||
|   languageExtractor: (item: NonNullable<Immutable<T>>) => string | undefined; | ||||
|   transform?: (item: NonNullable<Immutable<T>>) => NonNullable<Immutable<T>>; | ||||
| } | ||||
| 
 | ||||
| export default function useSmartLanguage<T>( | ||||
| function getPreferredLanguage( | ||||
|   preferredLanguages: (string | undefined)[], | ||||
|   availableLanguages: Map<string, number> | ||||
| ): number | undefined { | ||||
|   for (const locale of preferredLanguages) { | ||||
|     if (locale && availableLanguages.has(locale)) { | ||||
|       return availableLanguages.get(locale); | ||||
|     } | ||||
|   } | ||||
|   return undefined; | ||||
| } | ||||
| 
 | ||||
| export function useSmartLanguage<T>( | ||||
|   props: Props<T> | ||||
| ): [T | undefined, () => JSX.Element] { | ||||
| ): [Immutable<T | undefined>, () => JSX.Element] { | ||||
|   const { | ||||
|     items, | ||||
|     languageExtractor, | ||||
| @ -28,12 +40,15 @@ export default function useSmartLanguage<T>( | ||||
|   const [selectedTranslationIndex, setSelectedTranslationIndex] = useState< | ||||
|     number | undefined | ||||
|   >(); | ||||
|   const [selectedTranslation, setSelectedTranslation] = useState<T>(); | ||||
|   const [selectedTranslation, setSelectedTranslation] = | ||||
|     useState<Immutable<T>>(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     items.map((elem, index) => { | ||||
|       const result = languageExtractor(elem); | ||||
|       if (result !== undefined) availableLocales.set(result, index); | ||||
|       if (elem !== null && elem !== undefined) { | ||||
|         const result = languageExtractor(elem); | ||||
|         if (result !== undefined) availableLocales.set(result, index); | ||||
|       } | ||||
|     }); | ||||
|   }, [availableLocales, items, languageExtractor]); | ||||
| 
 | ||||
| @ -47,8 +62,9 @@ export default function useSmartLanguage<T>( | ||||
|   }, [appLayout.preferredLanguages, availableLocales, router.locale]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (selectedTranslationIndex !== undefined) | ||||
|     if (selectedTranslationIndex !== undefined) { | ||||
|       setSelectedTranslation(transform(items[selectedTranslationIndex])); | ||||
|     } | ||||
|   }, [items, selectedTranslationIndex, transform]); | ||||
| 
 | ||||
|   return [ | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| export default function useStateWithLocalStorage<T>( | ||||
| export function useStateWithLocalStorage<T>( | ||||
|   key: string, | ||||
|   initialValue: T | ||||
| ): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>] { | ||||
|  | ||||
| @ -1,14 +1,16 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel from "components/Panels/ContentPanel"; | ||||
| import { ContentPanel } from "components/Panels/ContentPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function FourOhFour(props: Props): JSX.Element { | ||||
| export default function FourOhFour(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
|  | ||||
| @ -1,14 +1,16 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel from "components/Panels/ContentPanel"; | ||||
| import { ContentPanel } from "components/Panels/ContentPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function FiveHundred(props: Props): JSX.Element { | ||||
| export default function FiveHundred(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
|  | ||||
| @ -6,7 +6,7 @@ import Document, { | ||||
|   NextScript, | ||||
| } from "next/document"; | ||||
| 
 | ||||
| class MyDocument extends Document { | ||||
| export default class MyDocument extends Document { | ||||
|   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | ||||
|   static async getInitialProps(ctx: DocumentContext) { | ||||
|     const initialProps = await Document.getInitialProps(ctx); | ||||
| @ -65,5 +65,3 @@ class MyDocument extends Document { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default MyDocument; | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| import PostPage, { Post } from "components/PostPage"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { PostPage } from "components/PostPage"; | ||||
| import { | ||||
|   getPostStaticProps, | ||||
|   PostStaticProps, | ||||
| } from "graphql/getPostStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   post: Post; | ||||
| } | ||||
| 
 | ||||
| export default function AccordsHandbook(props: Props): JSX.Element { | ||||
| export default function AccordsHandbook( | ||||
|   props: Immutable<PostStaticProps> | ||||
| ): JSX.Element { | ||||
|   const { post, langui, languages, currencies } = props; | ||||
|   return ( | ||||
|     <PostPage | ||||
| @ -23,21 +23,4 @@ export default function AccordsHandbook(props: Props): JSX.Element { | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export async function getStaticProps( | ||||
|   context: GetStaticPropsContext | ||||
| ): Promise<{ notFound: boolean } | { props: Props }> { | ||||
|   const sdk = getReadySdk(); | ||||
|   const slug = "accords-handbook"; | ||||
|   const post = await sdk.getPost({ | ||||
|     slug: slug, | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
|   if (!post.posts?.data[0].attributes) return { notFound: true }; | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     post: post.posts.data[0].attributes, | ||||
|   }; | ||||
|   return { | ||||
|     props: props, | ||||
|   }; | ||||
| } | ||||
| export const getStaticProps = getPostStaticProps("accords-handbook"); | ||||
|  | ||||
| @ -1,18 +1,18 @@ | ||||
| import InsetBox from "components/InsetBox"; | ||||
| import PostPage, { Post } from "components/PostPage"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { InsetBox } from "components/InsetBox"; | ||||
| import { PostPage } from "components/PostPage"; | ||||
| import { | ||||
|   getPostStaticProps, | ||||
|   PostStaticProps, | ||||
| } from "graphql/getPostStaticProps"; | ||||
| import { randomInt } from "helpers/numbers"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { RequestMailProps, ResponseMailProps } from "pages/api/mail"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { randomInt } from "queries/helpers"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   post: Post; | ||||
| } | ||||
| 
 | ||||
| export default function AboutUs(props: Props): JSX.Element { | ||||
| export default function AboutUs( | ||||
|   props: Immutable<PostStaticProps> | ||||
| ): JSX.Element { | ||||
|   const { post, langui, languages, currencies } = props; | ||||
| 
 | ||||
|   const router = useRouter(); | ||||
| @ -181,21 +181,4 @@ export default function AboutUs(props: Props): JSX.Element { | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export async function getStaticProps( | ||||
|   context: GetStaticPropsContext | ||||
| ): Promise<{ notFound: boolean } | { props: Props }> { | ||||
|   const sdk = getReadySdk(); | ||||
|   const slug = "contact"; | ||||
|   const post = await sdk.getPost({ | ||||
|     slug: slug, | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
|   if (!post.posts?.data[0].attributes) return { notFound: true }; | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     post: post.posts.data[0].attributes, | ||||
|   }; | ||||
|   return { | ||||
|     props: props, | ||||
|   }; | ||||
| } | ||||
| export const getStaticProps = getPostStaticProps("contact"); | ||||
|  | ||||
| @ -1,13 +1,14 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import NavOption from "components/PanelComponents/NavOption"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function AboutUs(props: Props): JSX.Element { | ||||
| export default function AboutUs(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|  | ||||
| @ -1,13 +1,10 @@ | ||||
| import PostPage, { Post } from "components/PostPage"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { PostPage } from "components/PostPage"; | ||||
| import { | ||||
|   getPostStaticProps, | ||||
|   PostStaticProps, | ||||
| } from "graphql/getPostStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   post: Post; | ||||
| } | ||||
| 
 | ||||
| export default function SiteInformation(props: Props): JSX.Element { | ||||
| export default function Legality(props: PostStaticProps): JSX.Element { | ||||
|   const { post, langui, languages, currencies } = props; | ||||
|   return ( | ||||
|     <PostPage | ||||
| @ -23,21 +20,4 @@ export default function SiteInformation(props: Props): JSX.Element { | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export async function getStaticProps( | ||||
|   context: GetStaticPropsContext | ||||
| ): Promise<{ notFound: boolean } | { props: Props }> { | ||||
|   const sdk = getReadySdk(); | ||||
|   const slug = "legality"; | ||||
|   const post = await sdk.getPost({ | ||||
|     slug: slug, | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
|   if (!post.posts?.data[0].attributes) return { notFound: true }; | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     post: post.posts.data[0].attributes, | ||||
|   }; | ||||
|   return { | ||||
|     props: props, | ||||
|   }; | ||||
| } | ||||
| export const getStaticProps = getPostStaticProps("legality"); | ||||
|  | ||||
| @ -1,12 +1,10 @@ | ||||
| import PostPage, { Post } from "components/PostPage"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { PostPage } from "components/PostPage"; | ||||
| import { | ||||
|   getPostStaticProps, | ||||
|   PostStaticProps, | ||||
| } from "graphql/getPostStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   post: Post; | ||||
| } | ||||
| export default function SharingPolicy(props: Props): JSX.Element { | ||||
| export default function SharingPolicy(props: PostStaticProps): JSX.Element { | ||||
|   const { post, langui, languages, currencies } = props; | ||||
|   return ( | ||||
|     <PostPage | ||||
| @ -22,21 +20,4 @@ export default function SharingPolicy(props: Props): JSX.Element { | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export async function getStaticProps( | ||||
|   context: GetStaticPropsContext | ||||
| ): Promise<{ notFound: boolean } | { props: Props }> { | ||||
|   const sdk = getReadySdk(); | ||||
|   const slug = "sharing-policy"; | ||||
|   const post = await sdk.getPost({ | ||||
|     slug: slug, | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
|   if (!post.posts?.data[0].attributes) return { notFound: true }; | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     post: post.posts.data[0].attributes, | ||||
|   }; | ||||
|   return { | ||||
|     props: props, | ||||
|   }; | ||||
| } | ||||
| export const getStaticProps = getPostStaticProps("sharing-policy"); | ||||
|  | ||||
| @ -14,7 +14,7 @@ export interface RequestMailProps { | ||||
|   formName: string; | ||||
| } | ||||
| 
 | ||||
| export default async function Mail( | ||||
| export async function Mail( | ||||
|   req: NextApiRequest, | ||||
|   res: NextApiResponse<ResponseMailProps> | ||||
| ) { | ||||
|  | ||||
| @ -70,7 +70,7 @@ type ResponseMailProps = { | ||||
|   revalidated: boolean; | ||||
| }; | ||||
| 
 | ||||
| export default async function Mail( | ||||
| export async function Mail( | ||||
|   req: NextApiRequest, | ||||
|   res: NextApiResponse<ResponseMailProps> | ||||
| ) { | ||||
|  | ||||
| @ -1,13 +1,14 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import NavOption from "components/PanelComponents/NavOption"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function Archives(props: Props): JSX.Element { | ||||
| export default function Archives(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|  | ||||
| @ -1,29 +1,30 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Switch from "components/Inputs/Switch"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel, { | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import ThumbnailPreview from "components/PreviewCard"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { PreviewCard } from "components/PreviewCard"; | ||||
| import { GetVideoChannelQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { getVideoThumbnailURL } from "helpers/videos"; | ||||
| import { | ||||
|   GetStaticPathsContext, | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { getVideoThumbnailURL } from "queries/helpers"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   channel: Exclude< | ||||
|     GetVideoChannelQuery["videoChannels"], | ||||
|     null | undefined | ||||
|   channel: NonNullable< | ||||
|     GetVideoChannelQuery["videoChannels"] | ||||
|   >["data"][number]["attributes"]; | ||||
| } | ||||
| 
 | ||||
| @ -35,7 +36,7 @@ export default function Channel(props: Props): JSX.Element { | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href="/archives/videos/" | ||||
|         title={"Videos"} | ||||
|         title={langui.videos} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.desktop} | ||||
|         className="mb-10" | ||||
| @ -43,12 +44,12 @@ export default function Channel(props: Props): JSX.Element { | ||||
| 
 | ||||
|       <PanelHeader | ||||
|         icon="movie" | ||||
|         title="Videos" | ||||
|         title={langui.videos} | ||||
|         description={langui.archives_description} | ||||
|       /> | ||||
| 
 | ||||
|       <div className="flex flex-row gap-2 place-items-center coarse:hidden"> | ||||
|         <p className="flex-shrink-0">{"Always show info"}:</p> | ||||
|         <p className="flex-shrink-0">{langui.always_show_info}:</p> | ||||
|         <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|       </div> | ||||
|     </SubPanel> | ||||
| @ -60,11 +61,15 @@ export default function Channel(props: Props): JSX.Element { | ||||
|         <h1 className="text-3xl">{channel?.title}</h1> | ||||
|         <p>{channel?.subscribers.toLocaleString()} subscribers</p> | ||||
|       </div> | ||||
|       <div className="grid gap-8 items-start mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"> | ||||
|       <div | ||||
|         className="grid gap-8 items-start mobile:grid-cols-2 | ||||
|         desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] | ||||
|         pb-12 border-b-[3px] border-dotted last-of-type:border-0" | ||||
|       > | ||||
|         {channel?.videos?.data.map((video) => ( | ||||
|           <> | ||||
|             {video.attributes && ( | ||||
|               <ThumbnailPreview | ||||
|               <PreviewCard | ||||
|                 key={video.id} | ||||
|                 href={`/archives/videos/v/${video.attributes.uid}`} | ||||
|                 title={video.attributes.title} | ||||
|  | ||||
| @ -1,24 +1,27 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import PageSelector from "components/Inputs/PageSelector"; | ||||
| import Switch from "components/Inputs/Switch"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { PageSelector } from "components/Inputs/PageSelector"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel, { | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import ThumbnailPreview from "components/PreviewCard"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { PreviewCard } from "components/PreviewCard"; | ||||
| import { GetVideosPreviewQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettyDate } from "helpers/formatters"; | ||||
| import { getVideoThumbnailURL } from "helpers/videos"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { getVideoThumbnailURL, prettyDate } from "queries/helpers"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   videos: Exclude<GetVideosPreviewQuery["videos"], null | undefined>["data"]; | ||||
|   videos: NonNullable<GetVideosPreviewQuery["videos"]>["data"]; | ||||
| } | ||||
| 
 | ||||
| export default function Videos(props: Props): JSX.Element { | ||||
| @ -64,7 +67,7 @@ export default function Videos(props: Props): JSX.Element { | ||||
|       /> | ||||
| 
 | ||||
|       <div className="flex flex-row gap-2 place-items-center coarse:hidden"> | ||||
|         <p className="flex-shrink-0">{"Always show info"}:</p> | ||||
|         <p className="flex-shrink-0">{langui.always_show_info}:</p> | ||||
|         <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|       </div> | ||||
|     </SubPanel> | ||||
| @ -79,11 +82,15 @@ export default function Videos(props: Props): JSX.Element { | ||||
|         className="mb-12" | ||||
|       /> | ||||
| 
 | ||||
|       <div className="grid gap-8 items-start thin:grid-cols-1 mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"> | ||||
|       <div | ||||
|         className="grid gap-8 items-start thin:grid-cols-1 mobile:grid-cols-2 | ||||
|         desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] | ||||
|         pb-12 border-b-[3px] border-dotted last-of-type:border-0" | ||||
|       > | ||||
|         {paginatedVideos[page].map((video) => ( | ||||
|           <> | ||||
|             {video.attributes && ( | ||||
|               <ThumbnailPreview | ||||
|               <PreviewCard | ||||
|                 key={video.id} | ||||
|                 href={`/archives/videos/v/${video.attributes.uid}`} | ||||
|                 title={video.attributes.title} | ||||
|  | ||||
| @ -1,34 +1,33 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import HorizontalLine from "components/HorizontalLine"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import InsetBox from "components/InsetBox"; | ||||
| import NavOption from "components/PanelComponents/NavOption"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { InsetBox } from "components/InsetBox"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel, { | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { GetVideoQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettyDate, prettyShortenNumber } from "helpers/formatters"; | ||||
| import { getVideoFile } from "helpers/videos"; | ||||
| import { useMediaMobile } from "hooks/useMediaQuery"; | ||||
| import { | ||||
|   GetStaticPathsContext, | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { getVideoFile, prettyDate, prettyShortenNumber } from "queries/helpers"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   video: Exclude< | ||||
|     Exclude< | ||||
|       GetVideoQuery["videos"], | ||||
|       null | undefined | ||||
|     >["data"][number]["attributes"], | ||||
|     null | undefined | ||||
|   video: NonNullable< | ||||
|     NonNullable<GetVideoQuery["videos"]>["data"][number]["attributes"] | ||||
|   >; | ||||
| } | ||||
| 
 | ||||
| @ -40,7 +39,7 @@ export default function Video(props: Props): JSX.Element { | ||||
|     <SubPanel> | ||||
|       <ReturnButton | ||||
|         href="/archives/videos/" | ||||
|         title={"Videos"} | ||||
|         title={langui.videos} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.desktop} | ||||
|         className="mb-10" | ||||
| @ -56,14 +55,14 @@ export default function Video(props: Props): JSX.Element { | ||||
|       /> | ||||
| 
 | ||||
|       <NavOption | ||||
|         title={"Channel"} | ||||
|         title={langui.channel} | ||||
|         url="#channel" | ||||
|         border | ||||
|         onClick={() => appLayout.setSubPanelOpen(false)} | ||||
|       /> | ||||
| 
 | ||||
|       <NavOption | ||||
|         title={"Description"} | ||||
|         title={langui.description} | ||||
|         url="#description" | ||||
|         border | ||||
|         onClick={() => appLayout.setSubPanelOpen(false)} | ||||
| @ -98,7 +97,8 @@ export default function Video(props: Props): JSX.Element { | ||||
|               className="w-full aspect-video" | ||||
|               title="YouTube video player" | ||||
|               frameBorder="0" | ||||
|               allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" | ||||
|               allow="accelerometer; autoplay; clipboard-write; | ||||
|               encrypted-media; gyroscope; picture-in-picture" | ||||
|               allowFullScreen | ||||
|             ></iframe> | ||||
|           )} | ||||
| @ -135,7 +135,7 @@ export default function Video(props: Props): JSX.Element { | ||||
|                 target="_blank" | ||||
|                 rel="noreferrer" | ||||
|               > | ||||
|                 <Button className="!py-0 !px-3">{`View on ${video.source}`}</Button> | ||||
|                 <Button className="!py-0 !px-3">{`${langui.view_on} ${video.source}`}</Button> | ||||
|               </a> | ||||
|             </div> | ||||
|           </div> | ||||
| @ -144,7 +144,7 @@ export default function Video(props: Props): JSX.Element { | ||||
|         {video.channel?.data?.attributes && ( | ||||
|           <InsetBox id="channel" className="grid place-items-center"> | ||||
|             <div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-4 text-center"> | ||||
|               <h2 className="text-2xl">{"Channel"}</h2> | ||||
|               <h2 className="text-2xl">{langui.channel}</h2> | ||||
|               <div> | ||||
|                 <Button | ||||
|                   href={`/archives/videos/c/${video.channel.data.attributes.uid}`} | ||||
| @ -153,8 +153,7 @@ export default function Video(props: Props): JSX.Element { | ||||
|                 </Button> | ||||
| 
 | ||||
|                 <p> | ||||
|                   {video.channel.data.attributes.subscribers.toLocaleString()}{" "} | ||||
|                   subscribers | ||||
|                   {`${video.channel.data.attributes.subscribers.toLocaleString()} ${langui.subscribers?.toLowerCase()}`} | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
| @ -163,7 +162,7 @@ export default function Video(props: Props): JSX.Element { | ||||
| 
 | ||||
|         <InsetBox id="description" className="grid place-items-center"> | ||||
|           <div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-8"> | ||||
|             <h2 className="text-2xl">{"Description"}</h2> | ||||
|             <h2 className="text-2xl">{langui.description}</h2> | ||||
|             <p className="whitespace-pre-line">{video.description}</p> | ||||
|           </div> | ||||
|         </InsetBox> | ||||
|  | ||||
| @ -1,12 +1,13 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function Chronicles(props: Props): JSX.Element { | ||||
| export default function Chronicles(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|  | ||||
| @ -1,56 +1,60 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Chip from "components/Chip"; | ||||
| import HorizontalLine from "components/HorizontalLine"; | ||||
| import Markdawn from "components/Markdown/Markdawn"; | ||||
| import TOC from "components/Markdown/TOC"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Markdawn } from "components/Markdown/Markdawn"; | ||||
| import { TOC } from "components/Markdown/TOC"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import PreviewLine from "components/PreviewLine"; | ||||
| import RecorderChip from "components/RecorderChip"; | ||||
| import ThumbnailHeader from "components/ThumbnailHeader"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { GetContentTextQuery } from "graphql/generated"; | ||||
| import { ContentPanel } from "components/Panels/ContentPanel"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { PreviewLine } from "components/PreviewLine"; | ||||
| import { RecorderChip } from "components/RecorderChip"; | ||||
| import { ThumbnailHeader } from "components/ThumbnailHeader"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { getNextContent, getPreviousContent } from "helpers/contents"; | ||||
| import { | ||||
|   prettyinlineTitle, | ||||
|   prettyLanguage, | ||||
|   prettySlug, | ||||
| } from "helpers/formatters"; | ||||
| import { getStatusDescription } from "helpers/others"; | ||||
| import { ContentWithTranslations, Immutable } from "helpers/types"; | ||||
| import { useMediaMobile } from "hooks/useMediaQuery"; | ||||
| import useSmartLanguage from "hooks/useSmartLanguage"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { | ||||
|   GetStaticPathsContext, | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { | ||||
|   getStatusDescription, | ||||
|   prettyinlineTitle, | ||||
|   prettyLanguage, | ||||
|   prettySlug, | ||||
| } from "queries/helpers"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   content: Exclude< | ||||
|     GetContentTextQuery["contents"], | ||||
|     null | undefined | ||||
|   >["data"][number]["attributes"]; | ||||
|   contentId: Exclude< | ||||
|     GetContentTextQuery["contents"], | ||||
|     null | undefined | ||||
|   >["data"][number]["id"]; | ||||
|   content: ContentWithTranslations; | ||||
| } | ||||
| 
 | ||||
| export default function Content(props: Props): JSX.Element { | ||||
| export default function Content(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui, content, languages } = props; | ||||
|   const isMobile = useMediaMobile(); | ||||
| 
 | ||||
|   const [selectedTextSet, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: content?.text_set, | ||||
|   const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({ | ||||
|     items: content.translations, | ||||
|     languages: languages, | ||||
|     languageExtractor: (item) => item?.language?.data?.attributes?.code, | ||||
|     languageExtractor: (item) => item.language?.data?.attributes?.code, | ||||
|   }); | ||||
| 
 | ||||
|   const selectedTitle = content?.titles?.[0]; | ||||
|   const previousContent = content.group?.data?.attributes?.contents | ||||
|     ? getPreviousContent( | ||||
|         content.group.data.attributes.contents.data, | ||||
|         content.slug | ||||
|       ) | ||||
|     : undefined; | ||||
| 
 | ||||
|   const nextContent = content.group?.data?.attributes?.contents | ||||
|     ? getNextContent(content.group.data.attributes.contents.data, content.slug) | ||||
|     : undefined; | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
| @ -62,124 +66,133 @@ export default function Content(props: Props): JSX.Element { | ||||
|         horizontalLine | ||||
|       /> | ||||
| 
 | ||||
|       {selectedTextSet?.source_language?.data?.attributes && ( | ||||
|       {selectedTranslation?.text_set && ( | ||||
|         <div className="grid gap-5"> | ||||
|           <h2 className="text-xl"> | ||||
|             {selectedTextSet.source_language.data.attributes.code === | ||||
|             selectedTextSet.language?.data?.attributes?.code | ||||
|             {selectedTranslation.text_set.source_language?.data?.attributes | ||||
|               ?.code === selectedTranslation.language?.data?.attributes?.code | ||||
|               ? langui.transcript_notice | ||||
|               : langui.translation_notice} | ||||
|           </h2> | ||||
| 
 | ||||
|           {selectedTextSet.source_language.data.attributes.code !== | ||||
|             selectedTextSet.language?.data?.attributes?.code && ( | ||||
|             <div className="grid place-items-center gap-2"> | ||||
|               <p className="font-headers">{langui.source_language}:</p> | ||||
|               <Chip> | ||||
|                 {prettyLanguage( | ||||
|                   selectedTextSet.source_language.data.attributes.code, | ||||
|                   languages | ||||
|                 )} | ||||
|               </Chip> | ||||
|             </div> | ||||
|           )} | ||||
|           {selectedTranslation.text_set.source_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-items-center place-content-center gap-2"> | ||||
|             <p className="font-headers">{langui.status}:</p> | ||||
| 
 | ||||
|             <ToolTip | ||||
|               content={getStatusDescription(selectedTextSet.status, langui)} | ||||
|               content={getStatusDescription( | ||||
|                 selectedTranslation.text_set.status, | ||||
|                 langui | ||||
|               )} | ||||
|               maxWidth={"20rem"} | ||||
|             > | ||||
|               <Chip>{selectedTextSet.status}</Chip> | ||||
|               <Chip>{selectedTranslation.text_set.status}</Chip> | ||||
|             </ToolTip> | ||||
|           </div> | ||||
| 
 | ||||
|           {selectedTextSet.transcribers && | ||||
|             selectedTextSet.transcribers.data.length > 0 && ( | ||||
|           {selectedTranslation.text_set.transcribers && | ||||
|             selectedTranslation.text_set.transcribers.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers">{langui.transcribers}:</p> | ||||
|                 <div className="grid place-items-center place-content-center gap-2"> | ||||
|                   {selectedTextSet.transcribers.data.map((recorder) => ( | ||||
|                     <> | ||||
|                       {recorder.attributes && ( | ||||
|                         <RecorderChip | ||||
|                           key={recorder.id} | ||||
|                           langui={langui} | ||||
|                           recorder={recorder.attributes} | ||||
|                         /> | ||||
|                       )} | ||||
|                     </> | ||||
|                   ))} | ||||
|                   {selectedTranslation.text_set.transcribers.data.map( | ||||
|                     (recorder) => ( | ||||
|                       <> | ||||
|                         {recorder.attributes && ( | ||||
|                           <RecorderChip | ||||
|                             key={recorder.id} | ||||
|                             langui={langui} | ||||
|                             recorder={recorder.attributes} | ||||
|                           /> | ||||
|                         )} | ||||
|                       </> | ||||
|                     ) | ||||
|                   )} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|           {selectedTextSet.translators && | ||||
|             selectedTextSet.translators.data.length > 0 && ( | ||||
|           {selectedTranslation.text_set.translators && | ||||
|             selectedTranslation.text_set.translators.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers">{langui.translators}:</p> | ||||
|                 <div className="grid place-items-center place-content-center gap-2"> | ||||
|                   {selectedTextSet.translators.data.map((recorder) => ( | ||||
|                     <> | ||||
|                       {recorder.attributes && ( | ||||
|                         <RecorderChip | ||||
|                           key={recorder.id} | ||||
|                           langui={langui} | ||||
|                           recorder={recorder.attributes} | ||||
|                         /> | ||||
|                       )} | ||||
|                     </> | ||||
|                   ))} | ||||
|                   {selectedTranslation.text_set.translators.data.map( | ||||
|                     (recorder) => ( | ||||
|                       <> | ||||
|                         {recorder.attributes && ( | ||||
|                           <RecorderChip | ||||
|                             key={recorder.id} | ||||
|                             langui={langui} | ||||
|                             recorder={recorder.attributes} | ||||
|                           /> | ||||
|                         )} | ||||
|                       </> | ||||
|                     ) | ||||
|                   )} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|           {selectedTextSet.proofreaders && | ||||
|             selectedTextSet.proofreaders.data.length > 0 && ( | ||||
|           {selectedTranslation.text_set.proofreaders && | ||||
|             selectedTranslation.text_set.proofreaders.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers">{langui.proofreaders}:</p> | ||||
|                 <div className="grid place-items-center place-content-center gap-2"> | ||||
|                   {selectedTextSet.proofreaders.data.map((recorder) => ( | ||||
|                     <> | ||||
|                       {recorder.attributes && ( | ||||
|                         <RecorderChip | ||||
|                           key={recorder.id} | ||||
|                           langui={langui} | ||||
|                           recorder={recorder.attributes} | ||||
|                         /> | ||||
|                       )} | ||||
|                     </> | ||||
|                   ))} | ||||
|                   {selectedTranslation.text_set.proofreaders.data.map( | ||||
|                     (recorder) => ( | ||||
|                       <> | ||||
|                         {recorder.attributes && ( | ||||
|                           <RecorderChip | ||||
|                             key={recorder.id} | ||||
|                             langui={langui} | ||||
|                             recorder={recorder.attributes} | ||||
|                           /> | ||||
|                         )} | ||||
|                       </> | ||||
|                     ) | ||||
|                   )} | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|           {selectedTextSet.notes && ( | ||||
|           {selectedTranslation.text_set.notes && ( | ||||
|             <div> | ||||
|               <p className="font-headers">{"Notes"}:</p> | ||||
|               <div className="grid place-items-center place-content-center gap-2"> | ||||
|                 <Markdawn text={selectedTextSet.notes} /> | ||||
|                 <Markdawn text={selectedTranslation.text_set.notes} /> | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {selectedTextSet && content?.text_set && selectedTextSet.text && ( | ||||
|       {selectedTranslation?.text_set?.text && ( | ||||
|         <> | ||||
|           <HorizontalLine /> | ||||
|           <TOC | ||||
|             text={selectedTextSet.text} | ||||
|             title={ | ||||
|               content.titles && content.titles.length > 0 && selectedTitle | ||||
|                 ? prettyinlineTitle( | ||||
|                     selectedTitle.pre_title, | ||||
|                     selectedTitle.title, | ||||
|                     selectedTitle.subtitle | ||||
|                   ) | ||||
|                 : prettySlug(content.slug) | ||||
|             } | ||||
|             text={selectedTranslation.text_set.text} | ||||
|             title={prettyinlineTitle( | ||||
|               selectedTranslation.pre_title, | ||||
|               selectedTranslation.title, | ||||
|               selectedTranslation.subtitle | ||||
|             )} | ||||
|           /> | ||||
|         </> | ||||
|       )} | ||||
| @ -188,142 +201,119 @@ export default function Content(props: Props): JSX.Element { | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
|       <ReturnButton | ||||
|         href={`/contents/${content?.slug}`} | ||||
|         href={`/contents/${content.slug}`} | ||||
|         title={langui.content} | ||||
|         langui={langui} | ||||
|         displayOn={ReturnButtonType.mobile} | ||||
|         className="mb-10" | ||||
|       /> | ||||
| 
 | ||||
|       {content && ( | ||||
|         <div className="grid place-items-center"> | ||||
|           <ThumbnailHeader | ||||
|             thumbnail={content.thumbnail?.data?.attributes} | ||||
|             pre_title={ | ||||
|               selectedTitle?.pre_title ?? content.titles?.[0]?.pre_title | ||||
|             } | ||||
|             title={selectedTitle?.title ?? content.titles?.[0]?.title} | ||||
|             subtitle={selectedTitle?.subtitle ?? content.titles?.[0]?.subtitle} | ||||
|             description={ | ||||
|               selectedTitle?.description ?? content.titles?.[0]?.description | ||||
|             } | ||||
|             type={content.type} | ||||
|             categories={content.categories} | ||||
|             langui={langui} | ||||
|             languageSwitcher={<LanguageSwitcher />} | ||||
|           /> | ||||
|       <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 />} | ||||
|         /> | ||||
| 
 | ||||
|           {content.previous_recommended?.data?.attributes && ( | ||||
|             <div className="mt-12 mb-8 w-full"> | ||||
|               <h2 className="text-center text-2xl mb-4">Previous content</h2> | ||||
|               <PreviewLine | ||||
|                 href={`/contents/${content.previous_recommended.data.attributes.slug}`} | ||||
|                 pre_title={ | ||||
|                   content.previous_recommended.data.attributes.titles?.[0] | ||||
|                     ?.pre_title | ||||
|                 } | ||||
|                 title={ | ||||
|                   content.previous_recommended.data.attributes.titles?.[0] | ||||
|                     ?.title ?? | ||||
|                   prettySlug(content.previous_recommended.data.attributes.slug) | ||||
|                 } | ||||
|                 subtitle={ | ||||
|                   content.previous_recommended.data.attributes.titles?.[0] | ||||
|                     ?.subtitle | ||||
|                 } | ||||
|                 thumbnail={ | ||||
|                   content.previous_recommended.data.attributes.thumbnail?.data | ||||
|                     ?.attributes | ||||
|                 } | ||||
|                 thumbnailAspectRatio="3/2" | ||||
|                 topChips={ | ||||
|                   isMobile | ||||
|                     ? undefined | ||||
|                     : content.previous_recommended.data.attributes.type?.data | ||||
|                         ?.attributes | ||||
|                     ? [ | ||||
|                         content.previous_recommended.data.attributes.type.data | ||||
|                           .attributes.titles?.[0] | ||||
|                           ? content.previous_recommended.data.attributes.type | ||||
|                               .data.attributes.titles[0]?.title | ||||
|                           : prettySlug( | ||||
|                               content.previous_recommended.data.attributes.type | ||||
|                                 .data.attributes.slug | ||||
|                             ), | ||||
|                       ] | ||||
|                     : undefined | ||||
|                 } | ||||
|                 bottomChips={ | ||||
|                   isMobile | ||||
|                     ? undefined | ||||
|                     : content.previous_recommended.data.attributes.categories?.data.map( | ||||
|                         (category) => category.attributes?.short ?? "" | ||||
|                       ) | ||||
|                 } | ||||
|               /> | ||||
|             </div> | ||||
|           )} | ||||
|         {previousContent?.attributes && ( | ||||
|           <div className="mt-12 mb-8 w-full"> | ||||
|             <h2 className="text-center text-2xl mb-4"> | ||||
|               {langui.previous_content} | ||||
|             </h2> | ||||
|             <PreviewLine | ||||
|               href={`/contents/${previousContent.attributes.slug}`} | ||||
|               pre_title={ | ||||
|                 previousContent.attributes.translations?.[0]?.pre_title | ||||
|               } | ||||
|               title={ | ||||
|                 previousContent.attributes.translations?.[0]?.title ?? | ||||
|                 prettySlug(previousContent.attributes.slug) | ||||
|               } | ||||
|               subtitle={previousContent.attributes.translations?.[0]?.subtitle} | ||||
|               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 /> | ||||
|         <HorizontalLine /> | ||||
| 
 | ||||
|           <Markdawn text={selectedTextSet?.text ?? ""} /> | ||||
|         <Markdawn text={selectedTranslation?.text_set?.text ?? ""} /> | ||||
| 
 | ||||
|           {content.next_recommended?.data?.attributes && ( | ||||
|             <> | ||||
|               <HorizontalLine /> | ||||
|               <h2 className="text-center text-2xl mb-4">Follow-up content</h2> | ||||
|               <PreviewLine | ||||
|                 href={`/contents/${content.next_recommended.data.attributes.slug}`} | ||||
|                 pre_title={ | ||||
|                   content.next_recommended.data.attributes.titles?.[0] | ||||
|                     ?.pre_title | ||||
|                 } | ||||
|                 title={ | ||||
|                   content.next_recommended.data.attributes.titles?.[0]?.title ?? | ||||
|                   prettySlug(content.next_recommended.data.attributes.slug) | ||||
|                 } | ||||
|                 subtitle={ | ||||
|                   content.next_recommended.data.attributes.titles?.[0]?.subtitle | ||||
|                 } | ||||
|                 thumbnail={ | ||||
|                   content.next_recommended.data.attributes.thumbnail?.data | ||||
|                     ?.attributes | ||||
|                 } | ||||
|                 thumbnailAspectRatio="3/2" | ||||
|                 topChips={ | ||||
|                   isMobile | ||||
|                     ? undefined | ||||
|                     : content.next_recommended.data.attributes.type?.data | ||||
|                         ?.attributes | ||||
|                     ? [ | ||||
|                         content.next_recommended.data.attributes.type.data | ||||
|                           .attributes.titles?.[0] | ||||
|                           ? content.next_recommended.data.attributes.type.data | ||||
|                               .attributes.titles[0]?.title | ||||
|                           : prettySlug( | ||||
|                               content.next_recommended.data.attributes.type.data | ||||
|                                 .attributes.slug | ||||
|                             ), | ||||
|                       ] | ||||
|                     : undefined | ||||
|                 } | ||||
|                 bottomChips={ | ||||
|                   isMobile | ||||
|                     ? undefined | ||||
|                     : content.next_recommended.data.attributes.categories?.data.map( | ||||
|                         (category) => category.attributes?.short ?? "" | ||||
|                       ) | ||||
|                 } | ||||
|               /> | ||||
|             </> | ||||
|           )} | ||||
|         </div> | ||||
|       )} | ||||
|         {nextContent?.attributes && ( | ||||
|           <> | ||||
|             <HorizontalLine /> | ||||
|             <h2 className="text-center text-2xl mb-4"> | ||||
|               {langui.followup_content} | ||||
|             </h2> | ||||
|             <PreviewLine | ||||
|               href={`/contents/${nextContent.attributes.slug}`} | ||||
|               pre_title={nextContent.attributes.translations?.[0]?.pre_title} | ||||
|               title={ | ||||
|                 nextContent.attributes.translations?.[0]?.title ?? | ||||
|                 prettySlug(nextContent.attributes.slug) | ||||
|               } | ||||
|               subtitle={nextContent.attributes.translations?.[0]?.subtitle} | ||||
|               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> | ||||
|   ); | ||||
| 
 | ||||
|   let description = ""; | ||||
|   if (content?.type?.data) { | ||||
|   if (content.type?.data) { | ||||
|     description += `${langui.type}: `; | ||||
| 
 | ||||
|     description += | ||||
| @ -332,7 +322,7 @@ export default function Content(props: Props): JSX.Element { | ||||
| 
 | ||||
|     description += "\n"; | ||||
|   } | ||||
|   if (content?.categories?.data && content.categories.data.length > 0) { | ||||
|   if (content.categories?.data && content.categories.data.length > 0) { | ||||
|     description += `${langui.categories}: `; | ||||
|     description += content.categories.data | ||||
|       .map((category) => category.attributes?.short) | ||||
| @ -343,15 +333,15 @@ export default function Content(props: Props): JSX.Element { | ||||
|   return ( | ||||
|     <AppLayout | ||||
|       navTitle={ | ||||
|         content?.titles && content.titles.length > 0 && content.titles[0] | ||||
|         selectedTranslation | ||||
|           ? prettyinlineTitle( | ||||
|               content.titles[0].pre_title, | ||||
|               content.titles[0].title, | ||||
|               content.titles[0].subtitle | ||||
|               selectedTranslation.pre_title, | ||||
|               selectedTranslation.title, | ||||
|               selectedTranslation.subtitle | ||||
|             ) | ||||
|           : prettySlug(content?.slug) | ||||
|           : prettySlug(content.slug) | ||||
|       } | ||||
|       thumbnail={content?.thumbnail?.data?.attributes ?? undefined} | ||||
|       thumbnail={content.thumbnail?.data?.attributes ?? undefined} | ||||
|       contentPanel={contentPanel} | ||||
|       subPanel={subPanel} | ||||
|       description={description} | ||||
| @ -370,12 +360,12 @@ export async function getStaticProps( | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
| 
 | ||||
|   if (!content.contents || content.contents.data.length === 0) | ||||
|   if (!content.contents || !content.contents.data[0].attributes?.translations) { | ||||
|     return { notFound: true }; | ||||
|   } | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     content: content.contents.data[0].attributes, | ||||
|     contentId: content.contents.data[0].id, | ||||
|     content: content.contents.data[0].attributes as ContentWithTranslations, | ||||
|   }; | ||||
|   return { | ||||
|     props: props, | ||||
|  | ||||
| @ -1,39 +1,51 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Chip from "components/Chip"; | ||||
| import Select from "components/Inputs/Select"; | ||||
| import Switch from "components/Inputs/Switch"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import ContentPanel, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Select } from "components/Inputs/Select"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import ThumbnailPreview from "components/PreviewCard"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { PreviewCard } from "components/PreviewCard"; | ||||
| import { GetContentsQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettyinlineTitle, prettySlug } from "helpers/formatters"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { prettyinlineTitle, prettySlug } from "queries/helpers"; | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   contents: Exclude<GetContentsQuery["contents"], null | undefined>["data"]; | ||||
|   contents: NonNullable<GetContentsQuery["contents"]>["data"]; | ||||
| } | ||||
| 
 | ||||
| type GroupContentItems = Map<string, Props["contents"]>; | ||||
| type GroupContentItems = Map<string, Immutable<Props["contents"]>>; | ||||
| 
 | ||||
| export default function Contents(props: Props): JSX.Element { | ||||
| export default function Contents(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui, contents } = props; | ||||
| 
 | ||||
|   const [groupingMethod, setGroupingMethod] = useState<number>(-1); | ||||
|   const [keepInfoVisible, setKeepInfoVisible] = useState(false); | ||||
| 
 | ||||
|   const [combineRelatedContent, setCombineRelatedContent] = useState(true); | ||||
| 
 | ||||
|   const [filteredItems, setFilteredItems] = useState( | ||||
|     filterContents(combineRelatedContent, contents) | ||||
|   ); | ||||
| 
 | ||||
|   const [groups, setGroups] = useState<GroupContentItems>( | ||||
|     getGroups(langui, groupingMethod, contents) | ||||
|     getGroups(langui, groupingMethod, filteredItems) | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setGroups(getGroups(langui, groupingMethod, contents)); | ||||
|   }, [langui, groupingMethod, contents]); | ||||
|     setFilteredItems(filterContents(combineRelatedContent, contents)); | ||||
|   }, [combineRelatedContent, contents]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setGroups(getGroups(langui, groupingMethod, filteredItems)); | ||||
|   }, [langui, groupingMethod, filteredItems]); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
| @ -55,7 +67,15 @@ export default function Contents(props: Props): JSX.Element { | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="flex flex-row gap-2 place-items-center coarse:hidden"> | ||||
|         <p className="flex-shrink-0">{"Always show info"}:</p> | ||||
|         <p className="flex-shrink-0">{langui.combine_related_contents}:</p> | ||||
|         <Switch | ||||
|           setState={setCombineRelatedContent} | ||||
|           state={combineRelatedContent} | ||||
|         /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="flex flex-row gap-2 place-items-center coarse:hidden"> | ||||
|         <p className="flex-shrink-0">{langui.always_show_info}:</p> | ||||
|         <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|       </div> | ||||
|     </SubPanel> | ||||
| @ -69,7 +89,8 @@ export default function Contents(props: Props): JSX.Element { | ||||
|               {name && ( | ||||
|                 <h2 | ||||
|                   key={`h2${name}`} | ||||
|                   className="text-2xl pb-2 pt-10 first-of-type:pt-0 flex flex-row place-items-center gap-2" | ||||
|                   className="text-2xl pb-2 pt-10 first-of-type:pt-0 | ||||
|                   flex flex-row place-items-center gap-2" | ||||
|                 > | ||||
|                   {name} | ||||
|                   <Chip>{`${items.length} ${ | ||||
| @ -81,22 +102,30 @@ export default function Contents(props: Props): JSX.Element { | ||||
|               )} | ||||
|               <div | ||||
|                 key={`items${name}`} | ||||
|                 className="grid gap-8 items-end grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]" | ||||
|                 className="grid gap-8 mobile:gap-4 items-end grid-cols-2 | ||||
|                 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]" | ||||
|               > | ||||
|                 {items.map((item) => ( | ||||
|                   <> | ||||
|                     {item.attributes && ( | ||||
|                       <ThumbnailPreview | ||||
|                       <PreviewCard | ||||
|                         key={item.id} | ||||
|                         href={`/contents/${item.attributes.slug}`} | ||||
|                         pre_title={item.attributes.titles?.[0]?.pre_title} | ||||
|                         pre_title={item.attributes.translations?.[0]?.pre_title} | ||||
|                         title={ | ||||
|                           item.attributes.titles?.[0]?.title ?? | ||||
|                           item.attributes.translations?.[0]?.title ?? | ||||
|                           prettySlug(item.attributes.slug) | ||||
|                         } | ||||
|                         subtitle={item.attributes.titles?.[0]?.subtitle} | ||||
|                         subtitle={item.attributes.translations?.[0]?.subtitle} | ||||
|                         thumbnail={item.attributes.thumbnail?.data?.attributes} | ||||
|                         thumbnailAspectRatio="3/2" | ||||
|                         stackNumber={ | ||||
|                           combineRelatedContent && | ||||
|                           item.attributes.group?.data?.attributes?.combine | ||||
|                             ? item.attributes.group.data.attributes.contents | ||||
|                                 ?.data.length | ||||
|                             : 0 | ||||
|                         } | ||||
|                         topChips={ | ||||
|                           item.attributes.type?.data?.attributes | ||||
|                             ? [ | ||||
| @ -143,18 +172,18 @@ export async function getStaticProps( | ||||
|   }); | ||||
|   if (!contents.contents) return { notFound: true }; | ||||
|   contents.contents.data.sort((a, b) => { | ||||
|     const titleA = a.attributes?.titles?.[0] | ||||
|     const titleA = a.attributes?.translations?.[0] | ||||
|       ? prettyinlineTitle( | ||||
|           a.attributes.titles[0].pre_title, | ||||
|           a.attributes.titles[0].title, | ||||
|           a.attributes.titles[0].subtitle | ||||
|           a.attributes.translations[0].pre_title, | ||||
|           a.attributes.translations[0].title, | ||||
|           a.attributes.translations[0].subtitle | ||||
|         ) | ||||
|       : a.attributes?.slug ?? ""; | ||||
|     const titleB = b.attributes?.titles?.[0] | ||||
|     const titleB = b.attributes?.translations?.[0] | ||||
|       ? prettyinlineTitle( | ||||
|           b.attributes.titles[0].pre_title, | ||||
|           b.attributes.titles[0].title, | ||||
|           b.attributes.titles[0].subtitle | ||||
|           b.attributes.translations[0].pre_title, | ||||
|           b.attributes.translations[0].title, | ||||
|           b.attributes.translations[0].subtitle | ||||
|         ) | ||||
|       : b.attributes?.slug ?? ""; | ||||
|     return titleA.localeCompare(titleB); | ||||
| @ -172,7 +201,7 @@ export async function getStaticProps( | ||||
| function getGroups( | ||||
|   langui: AppStaticProps["langui"], | ||||
|   groupByType: number, | ||||
|   items: Props["contents"] | ||||
|   items: Immutable<Props["contents"]> | ||||
| ): GroupContentItems { | ||||
|   switch (groupByType) { | ||||
|     case 0: { | ||||
| @ -209,15 +238,16 @@ function getGroups( | ||||
|     } | ||||
| 
 | ||||
|     case 1: { | ||||
|       const group: GroupContentItems = new Map(); | ||||
|       const group = new Map(); | ||||
|       items.map((item) => { | ||||
|         const type = | ||||
|           item.attributes?.type?.data?.attributes?.titles?.[0]?.title ?? | ||||
|           prettySlug(item.attributes?.type?.data?.attributes?.slug); | ||||
|           item.attributes?.type?.data?.attributes?.slug | ||||
|             ? prettySlug(item.attributes.type.data.attributes.slug) | ||||
|             : langui.no_type; | ||||
|         if (!group.has(type)) group.set(type, []); | ||||
|         group.get(type)?.push(item); | ||||
|       }); | ||||
| 
 | ||||
|       return group; | ||||
|     } | ||||
| 
 | ||||
| @ -228,3 +258,19 @@ function getGroups( | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function filterContents( | ||||
|   combineRelatedContent: boolean, | ||||
|   contents: Immutable<Props["contents"]> | ||||
| ): Immutable<Props["contents"]> { | ||||
|   if (combineRelatedContent) { | ||||
|     return [...contents].filter( | ||||
|       (content) => | ||||
|         !content.attributes?.group?.data?.attributes || | ||||
|         !content.attributes.group.data.attributes.combine || | ||||
|         content.attributes.group.data.attributes.contents?.data[0].id === | ||||
|           content.id | ||||
|     ); | ||||
|   } | ||||
|   return contents; | ||||
| } | ||||
|  | ||||
| @ -1,23 +1,22 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Chip from "components/Chip"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import ContentPanel, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { | ||||
|   DevGetContentsQuery, | ||||
|   Enum_Componentsetstextset_Status, | ||||
| } from "graphql/generated"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { DevGetContentsQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   contents: DevGetContentsQuery; | ||||
| } | ||||
| 
 | ||||
| export default function CheckupContents(props: Props): JSX.Element { | ||||
| export default function CheckupContents(props: Immutable<Props>): JSX.Element { | ||||
|   const { contents } = props; | ||||
|   const testReport = testingContent(contents); | ||||
| 
 | ||||
| @ -38,7 +37,8 @@ export default function CheckupContents(props: Props): JSX.Element { | ||||
|       {testReport.lines.map((line, index) => ( | ||||
|         <div | ||||
|           key={index} | ||||
|           className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] gap-2 items-center mb-2 justify-items-start" | ||||
|           className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] | ||||
|           gap-2 items-center mb-2 justify-items-start" | ||||
|         > | ||||
|           <Button | ||||
|             href={line.frontendUrl} | ||||
| @ -112,7 +112,7 @@ type ReportLine = { | ||||
|   frontendUrl: string; | ||||
| }; | ||||
| 
 | ||||
| function testingContent(contents: Props["contents"]): Report { | ||||
| function testingContent(contents: Immutable<Props["contents"]>): Report { | ||||
|   const report: Report = { | ||||
|     title: "Contents", | ||||
|     lines: [], | ||||
| @ -163,23 +163,6 @@ function testingContent(contents: Props["contents"]): Report { | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       if ( | ||||
|         content.attributes.next_recommended?.data?.id === content.id || | ||||
|         content.attributes.previous_recommended?.data?.id === content.id | ||||
|       ) { | ||||
|         report.lines.push({ | ||||
|           subitems: [content.attributes.slug], | ||||
|           name: "Self Recommendation", | ||||
|           type: "Error", | ||||
|           severity: "Very High", | ||||
|           description: | ||||
|             "The Content is referring to itself as a Next or Previous Recommended.", | ||||
|           recommandation: "", | ||||
|           backendUrl: backendUrl, | ||||
|           frontendUrl: frontendUrl, | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       if (!content.attributes.thumbnail?.data?.id) { | ||||
|         report.lines.push({ | ||||
|           subitems: [content.attributes.slug], | ||||
| @ -193,7 +176,7 @@ function testingContent(contents: Props["contents"]): Report { | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       if (content.attributes.titles?.length === 0) { | ||||
|       if (content.attributes.translations?.length === 0) { | ||||
|         report.lines.push({ | ||||
|           subitems: [content.attributes.slug], | ||||
|           name: "No Titles", | ||||
| @ -207,10 +190,10 @@ function testingContent(contents: Props["contents"]): Report { | ||||
|       } else { | ||||
|         const titleLanguages: string[] = []; | ||||
| 
 | ||||
|         content.attributes.titles?.map((title, titleIndex) => { | ||||
|           if (title && content.attributes) { | ||||
|             if (title.language?.data?.id) { | ||||
|               if (title.language.data.id in titleLanguages) { | ||||
|         content.attributes.translations?.map((translation, titleIndex) => { | ||||
|           if (translation && content.attributes) { | ||||
|             if (translation.language?.data?.id) { | ||||
|               if (translation.language.data.id in titleLanguages) { | ||||
|                 report.lines.push({ | ||||
|                   subitems: [ | ||||
|                     content.attributes.slug, | ||||
| @ -225,7 +208,7 @@ function testingContent(contents: Props["contents"]): Report { | ||||
|                   frontendUrl: frontendUrl, | ||||
|                 }); | ||||
|               } else { | ||||
|                 titleLanguages.push(title.language.data.id); | ||||
|                 titleLanguages.push(translation.language.data.id); | ||||
|               } | ||||
|             } else { | ||||
|               report.lines.push({ | ||||
| @ -242,7 +225,7 @@ function testingContent(contents: Props["contents"]): Report { | ||||
|                 frontendUrl: frontendUrl, | ||||
|               }); | ||||
|             } | ||||
|             if (!title.description) { | ||||
|             if (!translation.description) { | ||||
|               report.lines.push({ | ||||
|                 subitems: [ | ||||
|                   content.attributes.slug, | ||||
| @ -257,229 +240,199 @@ function testingContent(contents: Props["contents"]): Report { | ||||
|                 frontendUrl: frontendUrl, | ||||
|               }); | ||||
|             } | ||||
| 
 | ||||
|             if (translation.text_set) { | ||||
|               report.lines.push({ | ||||
|                 subitems: [content.attributes.slug], | ||||
|                 name: "No Text Set", | ||||
|                 type: "Missing", | ||||
|                 severity: "Medium", | ||||
|                 description: "The Content has no Text Set.", | ||||
|                 recommandation: "", | ||||
|                 backendUrl: backendUrl, | ||||
|                 frontendUrl: frontendUrl, | ||||
|               }); | ||||
|             } else { | ||||
|               /* | ||||
|                *const textSetLanguages: string[] = []; | ||||
|                *if (content.attributes && textSet) { | ||||
|                *  if (textSet.language?.data?.id) { | ||||
|                *    if (textSet.language.data.id in textSetLanguages) { | ||||
|                *      report.lines.push({ | ||||
|                *        subitems: [ | ||||
|                *          content.attributes.slug, | ||||
|                *          `TextSet ${textSetIndex.toString()}`, | ||||
|                *        ], | ||||
|                *        name: "Duplicate Language", | ||||
|                *        type: "Error", | ||||
|                *        severity: "High", | ||||
|                *        description: "", | ||||
|                *        recommandation: "", | ||||
|                *        backendUrl: backendUrl, | ||||
|                *        frontendUrl: frontendUrl, | ||||
|                *      }); | ||||
|                *    } else { | ||||
|                *      textSetLanguages.push(textSet.language.data.id); | ||||
|                *    } | ||||
|                *  } else { | ||||
|                *    report.lines.push({ | ||||
|                *      subitems: [ | ||||
|                *        content.attributes.slug, | ||||
|                *        `TextSet ${textSetIndex.toString()}`, | ||||
|                *      ], | ||||
|                *      name: "No Language", | ||||
|                *      type: "Error", | ||||
|                *      severity: "Very High", | ||||
|                *      description: "", | ||||
|                *      recommandation: "", | ||||
|                *      backendUrl: backendUrl, | ||||
|                *      frontendUrl: frontendUrl, | ||||
|                *    }); | ||||
|                *  } | ||||
|                * | ||||
|                *  if (!textSet.source_language?.data?.id) { | ||||
|                *    report.lines.push({ | ||||
|                *      subitems: [ | ||||
|                *        content.attributes.slug, | ||||
|                *        `TextSet ${textSetIndex.toString()}`, | ||||
|                *      ], | ||||
|                *      name: "No Source Language", | ||||
|                *      type: "Error", | ||||
|                *      severity: "High", | ||||
|                *      description: "", | ||||
|                *      recommandation: "", | ||||
|                *      backendUrl: backendUrl, | ||||
|                *      frontendUrl: frontendUrl, | ||||
|                *    }); | ||||
|                *  } | ||||
|                * | ||||
|                *  if (textSet.status !== Enum_Componentsetstextset_Status.Done) { | ||||
|                *    report.lines.push({ | ||||
|                *      subitems: [ | ||||
|                *        content.attributes.slug, | ||||
|                *        `TextSet ${textSetIndex.toString()}`, | ||||
|                *      ], | ||||
|                *      name: "Not Done Status", | ||||
|                *      type: "Improvement", | ||||
|                *      severity: "Low", | ||||
|                *      description: "", | ||||
|                *      recommandation: "", | ||||
|                *      backendUrl: backendUrl, | ||||
|                *      frontendUrl: frontendUrl, | ||||
|                *    }); | ||||
|                *  } | ||||
|                * | ||||
|                *  if (!textSet.text || textSet.text.length < 10) { | ||||
|                *    report.lines.push({ | ||||
|                *      subitems: [ | ||||
|                *        content.attributes.slug, | ||||
|                *        `TextSet ${textSetIndex.toString()}`, | ||||
|                *      ], | ||||
|                *      name: "No Text", | ||||
|                *      type: "Missing", | ||||
|                *      severity: "Medium", | ||||
|                *      description: "", | ||||
|                *      recommandation: "", | ||||
|                *      backendUrl: backendUrl, | ||||
|                *      frontendUrl: frontendUrl, | ||||
|                *    }); | ||||
|                *  } | ||||
|                * | ||||
|                *  if ( | ||||
|                *    textSet.source_language?.data?.id === | ||||
|                *    textSet.language?.data?.id | ||||
|                *  ) { | ||||
|                *    if (textSet.transcribers?.data.length === 0) { | ||||
|                *      report.lines.push({ | ||||
|                *        subitems: [ | ||||
|                *          content.attributes.slug, | ||||
|                *          `TextSet ${textSetIndex.toString()}`, | ||||
|                *        ], | ||||
|                *        name: "No Transcribers", | ||||
|                *        type: "Missing", | ||||
|                *        severity: "High", | ||||
|                *        description: | ||||
|                *          "The Content is a Transcription but doesn't credit any Transcribers.", | ||||
|                *        recommandation: "Add the appropriate Transcribers.", | ||||
|                *        backendUrl: backendUrl, | ||||
|                *        frontendUrl: frontendUrl, | ||||
|                *      }); | ||||
|                *    } | ||||
|                *    if ( | ||||
|                *      textSet.translators?.data && | ||||
|                *      textSet.translators.data.length > 0 | ||||
|                *    ) { | ||||
|                *      report.lines.push({ | ||||
|                *        subitems: [ | ||||
|                *          content.attributes.slug, | ||||
|                *          `TextSet ${textSetIndex.toString()}`, | ||||
|                *        ], | ||||
|                *        name: "Credited Translators", | ||||
|                *        type: "Error", | ||||
|                *        severity: "High", | ||||
|                *        description: | ||||
|                *          "The Content is a Transcription but credits one or more Translators.", | ||||
|                *        recommandation: | ||||
|                *          "If appropriate, create a Translation Text Set with the Translator credited there.", | ||||
|                *        backendUrl: backendUrl, | ||||
|                *        frontendUrl: frontendUrl, | ||||
|                *      }); | ||||
|                *    } | ||||
|                *  } else { | ||||
|                *    if (textSet.translators?.data.length === 0) { | ||||
|                *      report.lines.push({ | ||||
|                *        subitems: [ | ||||
|                *          content.attributes.slug, | ||||
|                *          `TextSet ${textSetIndex.toString()}`, | ||||
|                *        ], | ||||
|                *        name: "No Translators", | ||||
|                *        type: "Missing", | ||||
|                *        severity: "High", | ||||
|                *        description: | ||||
|                *          "The Content is a Transcription but doesn't credit any Translators.", | ||||
|                *        recommandation: "Add the appropriate Translators.", | ||||
|                *        backendUrl: backendUrl, | ||||
|                *        frontendUrl: frontendUrl, | ||||
|                *      }); | ||||
|                *    } | ||||
|                *    if ( | ||||
|                *      textSet.transcribers?.data && | ||||
|                *      textSet.transcribers.data.length > 0 | ||||
|                *    ) { | ||||
|                *      report.lines.push({ | ||||
|                *        subitems: [ | ||||
|                *          content.attributes.slug, | ||||
|                *          `TextSet ${textSetIndex.toString()}`, | ||||
|                *        ], | ||||
|                *        name: "Credited Transcribers", | ||||
|                *        type: "Error", | ||||
|                *        severity: "High", | ||||
|                *        description: | ||||
|                *          "The Content is a Translation but credits one or more Transcribers.", | ||||
|                *        recommandation: | ||||
|                *          "If appropriate, create a Transcription Text Set with the Transcribers credited there.", | ||||
|                *        backendUrl: backendUrl, | ||||
|                *        frontendUrl: frontendUrl, | ||||
|                *      }); | ||||
|                *    } | ||||
|                *  } | ||||
|                *} | ||||
|                */ | ||||
|             } | ||||
| 
 | ||||
|             report.lines.push({ | ||||
|               subitems: [content.attributes.slug], | ||||
|               name: "No Sets", | ||||
|               type: "Missing", | ||||
|               severity: "Medium", | ||||
|               description: "The Content has no Sets.", | ||||
|               recommandation: "", | ||||
|               backendUrl: backendUrl, | ||||
|               frontendUrl: frontendUrl, | ||||
|             }); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       if ( | ||||
|         content.attributes.text_set?.length === 0 && | ||||
|         content.attributes.audio_set?.length === 0 && | ||||
|         content.attributes.video_set?.length === 0 | ||||
|       ) { | ||||
|         report.lines.push({ | ||||
|           subitems: [content.attributes.slug], | ||||
|           name: "No Sets", | ||||
|           type: "Missing", | ||||
|           severity: "Medium", | ||||
|           description: "The Content has no Sets.", | ||||
|           recommandation: "", | ||||
|           backendUrl: backendUrl, | ||||
|           frontendUrl: frontendUrl, | ||||
|         }); | ||||
|       } else { | ||||
|         if (content.attributes.video_set?.length === 0) { | ||||
|           report.lines.push({ | ||||
|             subitems: [content.attributes.slug], | ||||
|             name: "No Video Sets", | ||||
|             type: "Missing", | ||||
|             severity: "Very Low", | ||||
|             description: "The Content has no Video Sets.", | ||||
|             recommandation: "", | ||||
|             backendUrl: backendUrl, | ||||
|             frontendUrl: frontendUrl, | ||||
|           }); | ||||
|         } | ||||
|         if (content.attributes.audio_set?.length === 0) { | ||||
|           report.lines.push({ | ||||
|             subitems: [content.attributes.slug], | ||||
|             name: "No Audio Sets", | ||||
|             type: "Missing", | ||||
|             severity: "Very Low", | ||||
|             description: "The Content has no Audio Sets.", | ||||
|             recommandation: "", | ||||
|             backendUrl: backendUrl, | ||||
|             frontendUrl: frontendUrl, | ||||
|           }); | ||||
|         } | ||||
|         if (content.attributes.text_set?.length === 0) { | ||||
|           report.lines.push({ | ||||
|             subitems: [content.attributes.slug], | ||||
|             name: "No Text Set", | ||||
|             type: "Missing", | ||||
|             severity: "Medium", | ||||
|             description: "The Content has no Text Set.", | ||||
|             recommandation: "", | ||||
|             backendUrl: backendUrl, | ||||
|             frontendUrl: frontendUrl, | ||||
|           }); | ||||
|         } else { | ||||
|           const textSetLanguages: string[] = []; | ||||
| 
 | ||||
|           content.attributes.text_set?.map((textSet, textSetIndex) => { | ||||
|             if (content.attributes && textSet) { | ||||
|               if (textSet.language?.data?.id) { | ||||
|                 if (textSet.language.data.id in textSetLanguages) { | ||||
|                   report.lines.push({ | ||||
|                     subitems: [ | ||||
|                       content.attributes.slug, | ||||
|                       `TextSet ${textSetIndex.toString()}`, | ||||
|                     ], | ||||
|                     name: "Duplicate Language", | ||||
|                     type: "Error", | ||||
|                     severity: "High", | ||||
|                     description: "", | ||||
|                     recommandation: "", | ||||
|                     backendUrl: backendUrl, | ||||
|                     frontendUrl: frontendUrl, | ||||
|                   }); | ||||
|                 } else { | ||||
|                   textSetLanguages.push(textSet.language.data.id); | ||||
|                 } | ||||
|               } else { | ||||
|                 report.lines.push({ | ||||
|                   subitems: [ | ||||
|                     content.attributes.slug, | ||||
|                     `TextSet ${textSetIndex.toString()}`, | ||||
|                   ], | ||||
|                   name: "No Language", | ||||
|                   type: "Error", | ||||
|                   severity: "Very High", | ||||
|                   description: "", | ||||
|                   recommandation: "", | ||||
|                   backendUrl: backendUrl, | ||||
|                   frontendUrl: frontendUrl, | ||||
|                 }); | ||||
|               } | ||||
| 
 | ||||
|               if (!textSet.source_language?.data?.id) { | ||||
|                 report.lines.push({ | ||||
|                   subitems: [ | ||||
|                     content.attributes.slug, | ||||
|                     `TextSet ${textSetIndex.toString()}`, | ||||
|                   ], | ||||
|                   name: "No Source Language", | ||||
|                   type: "Error", | ||||
|                   severity: "High", | ||||
|                   description: "", | ||||
|                   recommandation: "", | ||||
|                   backendUrl: backendUrl, | ||||
|                   frontendUrl: frontendUrl, | ||||
|                 }); | ||||
|               } | ||||
| 
 | ||||
|               if (textSet.status !== Enum_Componentsetstextset_Status.Done) { | ||||
|                 report.lines.push({ | ||||
|                   subitems: [ | ||||
|                     content.attributes.slug, | ||||
|                     `TextSet ${textSetIndex.toString()}`, | ||||
|                   ], | ||||
|                   name: "Not Done Status", | ||||
|                   type: "Improvement", | ||||
|                   severity: "Low", | ||||
|                   description: "", | ||||
|                   recommandation: "", | ||||
|                   backendUrl: backendUrl, | ||||
|                   frontendUrl: frontendUrl, | ||||
|                 }); | ||||
|               } | ||||
| 
 | ||||
|               if (!textSet.text || textSet.text.length < 10) { | ||||
|                 report.lines.push({ | ||||
|                   subitems: [ | ||||
|                     content.attributes.slug, | ||||
|                     `TextSet ${textSetIndex.toString()}`, | ||||
|                   ], | ||||
|                   name: "No Text", | ||||
|                   type: "Missing", | ||||
|                   severity: "Medium", | ||||
|                   description: "", | ||||
|                   recommandation: "", | ||||
|                   backendUrl: backendUrl, | ||||
|                   frontendUrl: frontendUrl, | ||||
|                 }); | ||||
|               } | ||||
| 
 | ||||
|               if ( | ||||
|                 textSet.source_language?.data?.id === textSet.language?.data?.id | ||||
|               ) { | ||||
|                 if (textSet.transcribers?.data.length === 0) { | ||||
|                   report.lines.push({ | ||||
|                     subitems: [ | ||||
|                       content.attributes.slug, | ||||
|                       `TextSet ${textSetIndex.toString()}`, | ||||
|                     ], | ||||
|                     name: "No Transcribers", | ||||
|                     type: "Missing", | ||||
|                     severity: "High", | ||||
|                     description: | ||||
|                       "The Content is a Transcription but doesn't credit any Transcribers.", | ||||
|                     recommandation: "Add the appropriate Transcribers.", | ||||
|                     backendUrl: backendUrl, | ||||
|                     frontendUrl: frontendUrl, | ||||
|                   }); | ||||
|                 } | ||||
|                 if ( | ||||
|                   textSet.translators?.data && | ||||
|                   textSet.translators.data.length > 0 | ||||
|                 ) { | ||||
|                   report.lines.push({ | ||||
|                     subitems: [ | ||||
|                       content.attributes.slug, | ||||
|                       `TextSet ${textSetIndex.toString()}`, | ||||
|                     ], | ||||
|                     name: "Credited Translators", | ||||
|                     type: "Error", | ||||
|                     severity: "High", | ||||
|                     description: | ||||
|                       "The Content is a Transcription but credits one or more Translators.", | ||||
|                     recommandation: | ||||
|                       "If appropriate, create a Translation Text Set with the Translator credited there.", | ||||
|                     backendUrl: backendUrl, | ||||
|                     frontendUrl: frontendUrl, | ||||
|                   }); | ||||
|                 } | ||||
|               } else { | ||||
|                 if (textSet.translators?.data.length === 0) { | ||||
|                   report.lines.push({ | ||||
|                     subitems: [ | ||||
|                       content.attributes.slug, | ||||
|                       `TextSet ${textSetIndex.toString()}`, | ||||
|                     ], | ||||
|                     name: "No Translators", | ||||
|                     type: "Missing", | ||||
|                     severity: "High", | ||||
|                     description: | ||||
|                       "The Content is a Transcription but doesn't credit any Translators.", | ||||
|                     recommandation: "Add the appropriate Translators.", | ||||
|                     backendUrl: backendUrl, | ||||
|                     frontendUrl: frontendUrl, | ||||
|                   }); | ||||
|                 } | ||||
|                 if ( | ||||
|                   textSet.transcribers?.data && | ||||
|                   textSet.transcribers.data.length > 0 | ||||
|                 ) { | ||||
|                   report.lines.push({ | ||||
|                     subitems: [ | ||||
|                       content.attributes.slug, | ||||
|                       `TextSet ${textSetIndex.toString()}`, | ||||
|                     ], | ||||
|                     name: "Credited Transcribers", | ||||
|                     type: "Error", | ||||
|                     severity: "High", | ||||
|                     description: | ||||
|                       "The Content is a Translation but credits one or more Transcribers.", | ||||
|                     recommandation: | ||||
|                       "If appropriate, create a Transcription Text Set with the Transcribers credited there.", | ||||
|                     backendUrl: backendUrl, | ||||
|                     frontendUrl: frontendUrl, | ||||
|                   }); | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|   return report; | ||||
|  | ||||
| @ -1,23 +1,27 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Chip from "components/Chip"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import ContentPanel, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { | ||||
|   DevGetLibraryItemsQuery, | ||||
|   Enum_Componentcollectionscomponentlibraryimages_Status, | ||||
| } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   libraryItems: DevGetLibraryItemsQuery; | ||||
| } | ||||
| 
 | ||||
| export default function CheckupLibraryItems(props: Props): JSX.Element { | ||||
| export default function CheckupLibraryItems( | ||||
|   props: Immutable<Props> | ||||
| ): JSX.Element { | ||||
|   const { libraryItems } = props; | ||||
|   const testReport = testingLibraryItem(libraryItems); | ||||
| 
 | ||||
| @ -38,7 +42,8 @@ export default function CheckupLibraryItems(props: Props): JSX.Element { | ||||
|       {testReport.lines.map((line, index) => ( | ||||
|         <div | ||||
|           key={index} | ||||
|           className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] gap-2 items-center mb-2 justify-items-start" | ||||
|           className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] | ||||
|           gap-2 items-center mb-2 justify-items-start" | ||||
|         > | ||||
|           <Button | ||||
|             href={line.frontendUrl} | ||||
| @ -113,7 +118,9 @@ type ReportLine = { | ||||
| }; | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
| function testingLibraryItem(libraryItems: Props["libraryItems"]): Report { | ||||
| function testingLibraryItem( | ||||
|   libraryItems: Immutable<Props["libraryItems"]> | ||||
| ): Report { | ||||
|   const report: Report = { | ||||
|     title: "Contents", | ||||
|     lines: [], | ||||
|  | ||||
| @ -1,19 +1,21 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import Markdawn from "components/Markdown/Markdawn"; | ||||
| import ContentPanel, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { Markdawn } from "components/Markdown/Markdawn"; | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import Popup from "components/Popup"; | ||||
| import ToolTip from "components/ToolTip"; | ||||
| import { Popup } from "components/Popup"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { useCallback, useState } from "react"; | ||||
| import TurndownService from "turndown"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function Editor(props: Props): JSX.Element { | ||||
| export default function Editor(props: Immutable<Props>): JSX.Element { | ||||
|   const handleInput = useCallback((text: string) => { | ||||
|     setMarkdown(text); | ||||
|   }, []); | ||||
| @ -337,7 +339,8 @@ export default function Editor(props: Props): JSX.Element { | ||||
|               const textarea = event.target as HTMLTextAreaElement; | ||||
|               handleInput(textarea.value); | ||||
|             }} | ||||
|             className="bg-mid !bg-opacity-40 rounded-xl outline-none p-8 w-full text-black font-monospace h-[70vh]" | ||||
|             className="bg-mid !bg-opacity-40 rounded-xl | ||||
|             outline-none p-8 w-full text-black font-monospace h-[70vh]" | ||||
|             value={markdown} | ||||
|             title="Input textarea" | ||||
|           /> | ||||
|  | ||||
| @ -1,10 +1,11 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function Gallery(props: Props): JSX.Element { | ||||
| export default function Gallery(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const contentPanel = ( | ||||
|     <iframe | ||||
|  | ||||
| @ -1,13 +1,11 @@ | ||||
| import PostPage, { Post } from "components/PostPage"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { PostPage } from "components/PostPage"; | ||||
| import { | ||||
|   getPostStaticProps, | ||||
|   PostStaticProps, | ||||
| } from "graphql/getPostStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   post: Post; | ||||
| } | ||||
| 
 | ||||
| export default function Home(props: Props): JSX.Element { | ||||
| export default function Home(props: Immutable<PostStaticProps>): JSX.Element { | ||||
|   const { post, langui, languages, currencies } = props; | ||||
|   return ( | ||||
|     <PostPage | ||||
| @ -17,7 +15,11 @@ export default function Home(props: Props): JSX.Element { | ||||
|       post={post} | ||||
|       prependBody={ | ||||
|         <div className="grid place-items-center place-content-center w-full gap-5 text-center"> | ||||
|           <div className="[mask:url('/icons/accords.svg')] [mask-size:contain] [mask-repeat:no-repeat] [mask-position:center] w-32 aspect-square mobile:w-[50vw] bg-black" /> | ||||
|           <div | ||||
|             className="[mask:url('/icons/accords.svg')] [mask-size:contain] | ||||
|             [mask-repeat:no-repeat] [mask-position:center] w-32 aspect-square | ||||
|             mobile:w-[50vw] bg-black" | ||||
|           /> | ||||
|           <h1 className="text-5xl mb-0">Accord’s Library</h1> | ||||
|           <h2 className="text-xl -mt-5"> | ||||
|             Discover • Analyze • Translate • Archive | ||||
| @ -30,21 +32,4 @@ export default function Home(props: Props): JSX.Element { | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export async function getStaticProps( | ||||
|   context: GetStaticPropsContext | ||||
| ): Promise<{ notFound: boolean } | { props: Props }> { | ||||
|   const sdk = getReadySdk(); | ||||
|   const slug = "home"; | ||||
|   const post = await sdk.getPost({ | ||||
|     slug: slug, | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
|   if (!post.posts?.data[0].attributes) return { notFound: true }; | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     post: post.posts.data[0].attributes, | ||||
|   }; | ||||
|   return { | ||||
|     props: props, | ||||
|   }; | ||||
| } | ||||
| export const getStaticProps = getPostStaticProps("home"); | ||||
|  | ||||
| @ -1,57 +1,59 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Chip from "components/Chip"; | ||||
| import Img, { getAssetURL, ImageQuality } from "components/Img"; | ||||
| import Button from "components/Inputs/Button"; | ||||
| import Switch from "components/Inputs/Switch"; | ||||
| import InsetBox from "components/InsetBox"; | ||||
| import ContentLine from "components/Library/ContentLine"; | ||||
| import LightBox from "components/LightBox"; | ||||
| import NavOption from "components/PanelComponents/NavOption"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Img } from "components/Img"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { InsetBox } from "components/InsetBox"; | ||||
| import { ContentLine } from "components/Library/ContentLine"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel, { | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import ThumbnailPreview from "components/PreviewCard"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { PreviewCard } from "components/PreviewCard"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { | ||||
|   Enum_Componentmetadatabooks_Binding_Type, | ||||
|   Enum_Componentmetadatabooks_Page_Order, | ||||
|   GetLibraryItemQuery, | ||||
| } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { | ||||
|   GetStaticPathsContext, | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { | ||||
|   convertMmToInch, | ||||
|   prettyDate, | ||||
|   prettyinlineTitle, | ||||
|   prettyItemSubType, | ||||
|   prettyItemType, | ||||
|   prettyPrice, | ||||
|   prettyURL, | ||||
|   sortContent, | ||||
| } from "queries/helpers"; | ||||
| } from "helpers/formatters"; | ||||
| import { getAssetURL, ImageQuality } from "helpers/img"; | ||||
| import { convertMmToInch } from "helpers/numbers"; | ||||
| import { sortContent } from "helpers/others"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useLightBox } from "hooks/useLightBox"; | ||||
| import { | ||||
|   GetStaticPathsContext, | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   item: Exclude< | ||||
|     GetLibraryItemQuery["libraryItems"], | ||||
|     null | undefined | ||||
|   item: NonNullable< | ||||
|     GetLibraryItemQuery["libraryItems"] | ||||
|   >["data"][number]["attributes"]; | ||||
|   itemId: Exclude< | ||||
|     GetLibraryItemQuery["libraryItems"], | ||||
|     null | undefined | ||||
|   itemId: NonNullable< | ||||
|     GetLibraryItemQuery["libraryItems"] | ||||
|   >["data"][number]["id"]; | ||||
| } | ||||
| 
 | ||||
| export default function LibrarySlug(props: Props): JSX.Element { | ||||
| export default function LibrarySlug(props: Immutable<Props>): JSX.Element { | ||||
|   const { item, langui, currencies } = props; | ||||
|   const appLayout = useAppLayout(); | ||||
| 
 | ||||
| @ -61,10 +63,7 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
| 
 | ||||
|   sortContent(item?.contents); | ||||
| 
 | ||||
|   const [lightboxOpen, setLightboxOpen] = useState(false); | ||||
|   const [lightboxImages, setLightboxImages] = useState([""]); | ||||
|   const [lightboxIndex, setLightboxIndex] = useState(0); | ||||
| 
 | ||||
|   const [openLightBox, LightBox] = useLightBox(); | ||||
|   const [keepInfoVisible, setKeepInfoVisible] = useState(false); | ||||
| 
 | ||||
|   let displayOpenScans = false; | ||||
| @ -134,13 +133,7 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.large}> | ||||
|       <LightBox | ||||
|         state={lightboxOpen} | ||||
|         setState={setLightboxOpen} | ||||
|         images={lightboxImages} | ||||
|         index={lightboxIndex} | ||||
|         setIndex={setLightboxIndex} | ||||
|       /> | ||||
|       <LightBox /> | ||||
| 
 | ||||
|       <ReturnButton | ||||
|         href="/library/" | ||||
| @ -151,17 +144,16 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|       /> | ||||
|       <div className="grid place-items-center gap-12"> | ||||
|         <div | ||||
|           className="drop-shadow-shade-xl w-full h-[50vh] mobile:h-[60vh] desktop:mb-16 relative cursor-pointer" | ||||
|           className="drop-shadow-shade-xl w-full h-[50vh] | ||||
|           mobile:h-[60vh] desktop:mb-16 relative cursor-pointer" | ||||
|           onClick={() => { | ||||
|             if (item?.thumbnail?.data?.attributes) { | ||||
|               setLightboxOpen(true); | ||||
|               setLightboxImages([ | ||||
|               openLightBox([ | ||||
|                 getAssetURL( | ||||
|                   item.thumbnail.data.attributes.url, | ||||
|                   ImageQuality.Large | ||||
|                 ), | ||||
|               ]); | ||||
|               setLightboxIndex(0); | ||||
|             } | ||||
|           }} | ||||
|         > | ||||
| @ -169,9 +161,7 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|             <Img | ||||
|               image={item.thumbnail.data.attributes} | ||||
|               quality={ImageQuality.Large} | ||||
|               layout="fill" | ||||
|               objectFit="contain" | ||||
|               priority | ||||
|               className="w-full h-full object-contain" | ||||
|             /> | ||||
|           ) : ( | ||||
|             <div className="w-full aspect-[21/29.7] bg-light rounded-xl"></div> | ||||
| @ -212,7 +202,7 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|               <> | ||||
|                 {item?.urls && item.urls.length ? ( | ||||
|                   <div className="flex flex-row place-items-center gap-3"> | ||||
|                     <p>Available at</p> | ||||
|                     <p>{langui.available_at}</p> | ||||
|                     {item.urls.map((url) => ( | ||||
|                       <> | ||||
|                         {url?.url && ( | ||||
| @ -228,7 +218,7 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 ) : ( | ||||
|                   <p>This item is not for sale or is no longer available</p> | ||||
|                   <p>{langui.item_not_available}</p> | ||||
|                 )} | ||||
|               </> | ||||
|             )} | ||||
| @ -238,13 +228,17 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|         {item?.gallery && item.gallery.data.length > 0 && ( | ||||
|           <div id="gallery" className="grid place-items-center gap-8  w-full"> | ||||
|             <h2 className="text-2xl">{langui.gallery}</h2> | ||||
|             <div className="grid w-full gap-8 items-end grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"> | ||||
|             <div | ||||
|               className="grid w-full gap-8 items-end | ||||
|               grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]" | ||||
|             > | ||||
|               {item.gallery.data.map((galleryItem, index) => ( | ||||
|                 <> | ||||
|                   {galleryItem.attributes && ( | ||||
|                     <div | ||||
|                       key={galleryItem.id} | ||||
|                       className="relative aspect-square hover:scale-[1.02] transition-transform cursor-pointer" | ||||
|                       className="relative aspect-square hover:scale-[1.02] | ||||
|                       transition-transform cursor-pointer" | ||||
|                       onClick={() => { | ||||
|                         if (item.gallery?.data) { | ||||
|                           const images: string[] = []; | ||||
| @ -257,18 +251,14 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|                                 ) | ||||
|                               ); | ||||
|                           }); | ||||
|                           setLightboxOpen(true); | ||||
|                           setLightboxImages(images); | ||||
|                           setLightboxIndex(index); | ||||
|                           openLightBox(images, index); | ||||
|                         } | ||||
|                       }} | ||||
|                     > | ||||
|                       <div className="bg-light absolute inset-0 rounded-lg drop-shadow-shade-md"></div> | ||||
|                       <Img | ||||
|                         className="rounded-lg" | ||||
|                         className="bg-light rounded-lg drop-shadow-shade-md | ||||
|                         w-full h-full object-cover" | ||||
|                         image={galleryItem.attributes} | ||||
|                         layout="fill" | ||||
|                         objectFit="cover" | ||||
|                       /> | ||||
|                     </div> | ||||
|                   )} | ||||
| @ -425,14 +415,17 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|             </h2> | ||||
| 
 | ||||
|             <div className="-mt-6 mb-8 flex flex-row gap-2 place-items-center coarse:hidden"> | ||||
|               <p className="flex-shrink-0">{"Always show info"}:</p> | ||||
|               <p className="flex-shrink-0">{langui.always_show_info}:</p> | ||||
|               <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|             </div> | ||||
|             <div className="grid gap-8 items-end mobile:grid-cols-2 grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] w-full"> | ||||
|             <div | ||||
|               className="grid gap-8 items-end mobile:grid-cols-2 | ||||
|               grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] w-full" | ||||
|             > | ||||
|               {item.subitems.data.map((subitem) => ( | ||||
|                 <> | ||||
|                   {subitem.attributes && ( | ||||
|                     <ThumbnailPreview | ||||
|                     <PreviewCard | ||||
|                       key={subitem.id} | ||||
|                       href={`/library/${subitem.attributes.slug}`} | ||||
|                       title={subitem.attributes.title} | ||||
|  | ||||
| @ -1,47 +1,46 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import ScanSet from "components/Library/ScanSet"; | ||||
| import ScanSetCover from "components/Library/ScanSetCover"; | ||||
| import LightBox from "components/LightBox"; | ||||
| import NavOption from "components/PanelComponents/NavOption"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { ScanSet } from "components/Library/ScanSet"; | ||||
| import { ScanSetCover } from "components/Library/ScanSetCover"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel, { | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { GetLibraryItemScansQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettyinlineTitle, prettySlug } from "helpers/formatters"; | ||||
| import { sortContent } from "helpers/others"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { useLightBox } from "hooks/useLightBox"; | ||||
| import { | ||||
|   GetStaticPathsContext, | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { prettyinlineTitle, prettySlug, sortContent } from "queries/helpers"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   item: Exclude< | ||||
|     GetLibraryItemScansQuery["libraryItems"], | ||||
|     null | undefined | ||||
|   item: NonNullable< | ||||
|     GetLibraryItemScansQuery["libraryItems"] | ||||
|   >["data"][number]["attributes"]; | ||||
|   itemId: Exclude< | ||||
|     GetLibraryItemScansQuery["libraryItems"], | ||||
|     null | undefined | ||||
|   itemId: NonNullable< | ||||
|     GetLibraryItemScansQuery["libraryItems"] | ||||
|   >["data"][number]["id"]; | ||||
| } | ||||
| 
 | ||||
| export default function LibrarySlug(props: Props): JSX.Element { | ||||
| export default function LibrarySlug(props: Immutable<Props>): JSX.Element { | ||||
|   const { item, langui, languages } = props; | ||||
|   const appLayout = useAppLayout(); | ||||
| 
 | ||||
|   sortContent(item?.contents); | ||||
| 
 | ||||
|   const [lightboxOpen, setLightboxOpen] = useState(false); | ||||
|   const [lightboxImages, setLightboxImages] = useState([""]); | ||||
|   const [lightboxIndex, setLightboxIndex] = useState(0); | ||||
|   const [openLightBox, LightBox] = useLightBox(); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
| @ -61,7 +60,9 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|           subtitle={ | ||||
|             content.attributes?.range[0]?.__typename === | ||||
|             "ComponentRangePageRange" | ||||
|               ? `${content.attributes.range[0].starting_page} → ${content.attributes.range[0].ending_page}` | ||||
|               ? `${content.attributes.range[0].starting_page}` + | ||||
|                 `→` + | ||||
|                 `${content.attributes.range[0].ending_page}` | ||||
|               : undefined | ||||
|           } | ||||
|           onClick={() => appLayout.setSubPanelOpen(false)} | ||||
| @ -73,13 +74,7 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.large}> | ||||
|       <LightBox | ||||
|         state={lightboxOpen} | ||||
|         setState={setLightboxOpen} | ||||
|         images={lightboxImages} | ||||
|         index={lightboxIndex} | ||||
|         setIndex={setLightboxIndex} | ||||
|       /> | ||||
|       <LightBox /> | ||||
| 
 | ||||
|       <ReturnButton | ||||
|         href={`/library/${item?.slug}`} | ||||
| @ -92,9 +87,7 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|       {item?.images && ( | ||||
|         <ScanSetCover | ||||
|           images={item.images} | ||||
|           setLightboxImages={setLightboxImages} | ||||
|           setLightboxIndex={setLightboxIndex} | ||||
|           setLightboxOpen={setLightboxOpen} | ||||
|           openLightBox={openLightBox} | ||||
|           languages={languages} | ||||
|           langui={langui} | ||||
|         /> | ||||
| @ -106,9 +99,7 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
|             <ScanSet | ||||
|               key={content.id} | ||||
|               scanSet={content.attributes.scan_set} | ||||
|               setLightboxImages={setLightboxImages} | ||||
|               setLightboxIndex={setLightboxIndex} | ||||
|               setLightboxOpen={setLightboxOpen} | ||||
|               openLightBox={openLightBox} | ||||
|               slug={content.attributes.slug} | ||||
|               title={prettySlug(content.attributes.slug, item.slug)} | ||||
|               languages={languages} | ||||
|  | ||||
| @ -1,35 +1,34 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Chip from "components/Chip"; | ||||
| import Select from "components/Inputs/Select"; | ||||
| import Switch from "components/Inputs/Switch"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import ContentPanel, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Select } from "components/Inputs/Select"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import ThumbnailPreview from "components/PreviewCard"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { PreviewCard } from "components/PreviewCard"; | ||||
| import { GetLibraryItemsPreviewQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { | ||||
|   convertPrice, | ||||
|   prettyDate, | ||||
|   prettyinlineTitle, | ||||
|   prettyItemSubType, | ||||
| } from "queries/helpers"; | ||||
| } from "helpers/formatters"; | ||||
| import { convertPrice } from "helpers/numbers"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   items: Exclude< | ||||
|     GetLibraryItemsPreviewQuery["libraryItems"], | ||||
|     null | undefined | ||||
|   >["data"]; | ||||
|   items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"]; | ||||
| } | ||||
| 
 | ||||
| type GroupLibraryItems = Map<string, Props["items"]>; | ||||
| type GroupLibraryItems = Map<string, Immutable<Props["items"]>>; | ||||
| 
 | ||||
| export default function Library(props: Props): JSX.Element { | ||||
| export default function Library(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui, items: libraryItems, currencies } = props; | ||||
| 
 | ||||
|   const [showSubitems, setShowSubitems] = useState<boolean>(false); | ||||
| @ -39,7 +38,7 @@ export default function Library(props: Props): JSX.Element { | ||||
|   const [groupingMethod, setGroupingMethod] = useState<number>(-1); | ||||
|   const [keepInfoVisible, setKeepInfoVisible] = useState(false); | ||||
| 
 | ||||
|   const [filteredItems, setFilteredItems] = useState<Props["items"]>( | ||||
|   const [filteredItems, setFilteredItems] = useState( | ||||
|     filterItems( | ||||
|       showSubitems, | ||||
|       showPrimaryItems, | ||||
| @ -48,11 +47,11 @@ export default function Library(props: Props): JSX.Element { | ||||
|     ) | ||||
|   ); | ||||
| 
 | ||||
|   const [sortedItems, setSortedItem] = useState<Props["items"]>( | ||||
|   const [sortedItems, setSortedItem] = useState( | ||||
|     sortBy(groupingMethod, filteredItems, currencies) | ||||
|   ); | ||||
| 
 | ||||
|   const [groups, setGroups] = useState<GroupLibraryItems>( | ||||
|   const [groups, setGroups] = useState( | ||||
|     getGroups(langui, groupingMethod, sortedItems) | ||||
|   ); | ||||
| 
 | ||||
| @ -128,7 +127,7 @@ export default function Library(props: Props): JSX.Element { | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="flex flex-row gap-2 place-items-center coarse:hidden"> | ||||
|         <p className="flex-shrink-0">{"Always show info"}:</p> | ||||
|         <p className="flex-shrink-0">{langui.always_show_info}:</p> | ||||
|         <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|       </div> | ||||
|     </SubPanel> | ||||
| @ -142,7 +141,8 @@ export default function Library(props: Props): JSX.Element { | ||||
|               {name && ( | ||||
|                 <h2 | ||||
|                   key={`h2${name}`} | ||||
|                   className="text-2xl pb-2 pt-10 first-of-type:pt-0 flex flex-row place-items-center gap-2" | ||||
|                   className="text-2xl pb-2 pt-10 first-of-type:pt-0 | ||||
|                   flex flex-row place-items-center gap-2" | ||||
|                 > | ||||
|                   {name} | ||||
|                   <Chip>{`${items.length} ${ | ||||
| @ -154,12 +154,14 @@ export default function Library(props: Props): JSX.Element { | ||||
|               )} | ||||
|               <div | ||||
|                 key={`items${name}`} | ||||
|                 className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0" | ||||
|                 className="grid gap-8 mobile:gap-4 items-end mobile:grid-cols-2 | ||||
|                 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))] | ||||
|                 pb-12 border-b-[3px] border-dotted last-of-type:border-0" | ||||
|               > | ||||
|                 {items.map((item) => ( | ||||
|                   <> | ||||
|                     {item.attributes && ( | ||||
|                       <ThumbnailPreview | ||||
|                       <PreviewCard | ||||
|                         key={item.id} | ||||
|                         href={`/library/${item.attributes.slug}`} | ||||
|                         title={item.attributes.title} | ||||
| @ -224,7 +226,7 @@ export async function getStaticProps( | ||||
| function getGroups( | ||||
|   langui: AppStaticProps["langui"], | ||||
|   groupByType: number, | ||||
|   items: Props["items"] | ||||
|   items: Immutable<Props["items"]> | ||||
| ): GroupLibraryItems { | ||||
|   switch (groupByType) { | ||||
|     case 0: { | ||||
| @ -262,7 +264,7 @@ function getGroups( | ||||
|     } | ||||
| 
 | ||||
|     case 1: { | ||||
|       const group: GroupLibraryItems = new Map(); | ||||
|       const group = new Map(); | ||||
|       group.set(langui.audio ?? "Audio", []); | ||||
|       group.set(langui.game ?? "Game", []); | ||||
|       group.set(langui.textual ?? "Textual", []); | ||||
| @ -334,7 +336,7 @@ function getGroups( | ||||
|             years.push(item.attributes.release_date.year); | ||||
|         } | ||||
|       }); | ||||
|       const group: GroupLibraryItems = new Map(); | ||||
|       const group = new Map(); | ||||
|       years.sort((a, b) => a - b); | ||||
|       years.map((year) => { | ||||
|         group.set(year.toString(), []); | ||||
| @ -352,7 +354,7 @@ function getGroups( | ||||
|     } | ||||
| 
 | ||||
|     default: { | ||||
|       const group: GroupLibraryItems = new Map(); | ||||
|       const group = new Map(); | ||||
|       group.set("", items); | ||||
|       return group; | ||||
|     } | ||||
| @ -363,8 +365,8 @@ function filterItems( | ||||
|   showSubitems: boolean, | ||||
|   showPrimaryItems: boolean, | ||||
|   showSecondaryItems: boolean, | ||||
|   items: Props["items"] | ||||
| ): Props["items"] { | ||||
|   items: Immutable<Props["items"]> | ||||
| ): Immutable<Props["items"]> { | ||||
|   return [...items].filter((item) => { | ||||
|     if (!showSubitems && !item.attributes?.root_item) return false; | ||||
|     if ( | ||||
| @ -384,9 +386,9 @@ function filterItems( | ||||
| 
 | ||||
| function sortBy( | ||||
|   orderByType: number, | ||||
|   items: Props["items"], | ||||
|   items: Immutable<Props["items"]>, | ||||
|   currencies: AppStaticProps["currencies"] | ||||
| ): Props["items"] { | ||||
| ): Immutable<Props["items"]> { | ||||
|   switch (orderByType) { | ||||
|     case 0: | ||||
|       return [...items].sort((a, b) => { | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| export default function Merch(props: Props): JSX.Element { | ||||
| export default function Merch(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|  | ||||
| @ -1,22 +1,20 @@ | ||||
| import PostPage, { Post } from "components/PostPage"; | ||||
| import { GetPostQuery } from "graphql/generated"; | ||||
| import { PostPage } from "components/PostPage"; | ||||
| import { AppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { | ||||
|   getPostStaticProps, | ||||
|   PostStaticProps, | ||||
| } from "graphql/getPostStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { | ||||
|   GetStaticPathsContext, | ||||
|   GetStaticPathsResult, | ||||
|   GetStaticPropsContext, | ||||
| } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   post: Post; | ||||
|   postId: Exclude< | ||||
|     GetPostQuery["posts"], | ||||
|     null | undefined | ||||
|   >["data"][number]["id"]; | ||||
| } | ||||
| interface Props extends AppStaticProps, PostStaticProps {} | ||||
| 
 | ||||
| export default function LibrarySlug(props: Props): JSX.Element { | ||||
| export default function LibrarySlug(props: Immutable<Props>): JSX.Element { | ||||
|   const { post, langui, languages, currencies } = props; | ||||
|   return ( | ||||
|     <PostPage | ||||
| @ -36,21 +34,8 @@ export default function LibrarySlug(props: Props): JSX.Element { | ||||
| export async function getStaticProps( | ||||
|   context: GetStaticPropsContext | ||||
| ): Promise<{ notFound: boolean } | { props: Props }> { | ||||
|   const sdk = getReadySdk(); | ||||
|   const slug = context.params?.slug ? context.params.slug.toString() : ""; | ||||
|   const post = await sdk.getPost({ | ||||
|     slug: slug, | ||||
|     language_code: context.locale ?? "en", | ||||
|   }); | ||||
|   if (!post.posts?.data[0].attributes) return { notFound: true }; | ||||
|   const props: Props = { | ||||
|     ...(await getAppStaticProps(context)), | ||||
|     post: post.posts.data[0].attributes, | ||||
|     postId: post.posts.data[0].id, | ||||
|   }; | ||||
|   return { | ||||
|     props: props, | ||||
|   }; | ||||
|   return await getPostStaticProps(slug)(context); | ||||
| } | ||||
| 
 | ||||
| export async function getStaticPaths( | ||||
|  | ||||
| @ -1,35 +1,30 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import Switch from "components/Inputs/Switch"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import ContentPanel, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { | ||||
|   ContentPanel, | ||||
|   ContentPanelWidthSizes, | ||||
| } from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import ThumbnailPreview from "components/PreviewCard"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { PreviewCard } from "components/PreviewCard"; | ||||
| import { GetPostsPreviewQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettyDate, prettySlug } from "helpers/formatters"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { prettyDate, prettySlug } from "queries/helpers"; | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   posts: Exclude<GetPostsPreviewQuery["posts"], null | undefined>["data"]; | ||||
|   posts: NonNullable<GetPostsPreviewQuery["posts"]>["data"]; | ||||
| } | ||||
| 
 | ||||
| export default function News(props: Props): JSX.Element { | ||||
|   const { langui, posts } = props; | ||||
| export default function News(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const posts = sortPosts(props.posts); | ||||
| 
 | ||||
|   const [keepInfoVisible, setKeepInfoVisible] = useState(true); | ||||
| 
 | ||||
|   posts | ||||
|     .sort((a, b) => { | ||||
|       const dateA = a.attributes?.date ? prettyDate(a.attributes.date) : "9999"; | ||||
|       const dateB = b.attributes?.date ? prettyDate(b.attributes.date) : "9999"; | ||||
|       return dateA.localeCompare(dateB); | ||||
|     }) | ||||
|     .reverse(); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <PanelHeader | ||||
| @ -39,7 +34,7 @@ export default function News(props: Props): JSX.Element { | ||||
|       /> | ||||
| 
 | ||||
|       <div className="flex flex-row gap-2 place-items-center coarse:hidden"> | ||||
|         <p className="flex-shrink-0">{"Always show info"}:</p> | ||||
|         <p className="flex-shrink-0">{langui.always_show_info}:</p> | ||||
|         <Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> | ||||
|       </div> | ||||
|     </SubPanel> | ||||
| @ -47,11 +42,14 @@ export default function News(props: Props): JSX.Element { | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.large}> | ||||
|       <div className="grid gap-8 items-end grid-cols-1 desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]"> | ||||
|       <div | ||||
|         className="grid gap-8 items-end grid-cols-1 | ||||
|         desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]" | ||||
|       > | ||||
|         {posts.map((post) => ( | ||||
|           <> | ||||
|             {post.attributes && ( | ||||
|               <ThumbnailPreview | ||||
|               <PreviewCard | ||||
|                 key={post.id} | ||||
|                 href={`/news/${post.attributes.slug}`} | ||||
|                 title={ | ||||
| @ -103,3 +101,17 @@ export async function getStaticProps( | ||||
|     props: props, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function sortPosts( | ||||
|   posts: Immutable<Props["posts"]> | ||||
| ): Immutable<Props["posts"]> { | ||||
|   const sortedPosts = [...posts] as Props["posts"]; | ||||
|   sortedPosts | ||||
|     .sort((a, b) => { | ||||
|       const dateA = a.attributes?.date ? prettyDate(a.attributes.date) : "9999"; | ||||
|       const dateB = b.attributes?.date ? prettyDate(b.attributes.date) : "9999"; | ||||
|       return dateA.localeCompare(dateB); | ||||
|     }) | ||||
|     .reverse(); | ||||
|   return sortedPosts as Immutable<Props["posts"]>; | ||||
| } | ||||
|  | ||||
| @ -1,28 +1,25 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import InsetBox from "components/InsetBox"; | ||||
| import NavOption from "components/PanelComponents/NavOption"; | ||||
| import ReturnButton, { | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { InsetBox } from "components/InsetBox"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { | ||||
|   ReturnButton, | ||||
|   ReturnButtonType, | ||||
| } from "components/PanelComponents/ReturnButton"; | ||||
| import ContentPanel from "components/Panels/ContentPanel"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import ChronologyYearComponent from "components/Wiki/Chronology/ChronologyYearComponent"; | ||||
| import { ContentPanel } from "components/Panels/ContentPanel"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { ChronologyYearComponent } from "components/Wiki/Chronology/ChronologyYearComponent"; | ||||
| import { useAppLayout } from "contexts/AppLayoutContext"; | ||||
| import { GetChronologyItemsQuery, GetErasQuery } from "graphql/generated"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| import { prettySlug } from "queries/helpers"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps { | ||||
|   chronologyItems: Exclude< | ||||
|     GetChronologyItemsQuery["chronologyItems"], | ||||
|     null | undefined | ||||
|   >["data"]; | ||||
|   chronologyEras: Exclude< | ||||
|     GetErasQuery["chronologyEras"], | ||||
|     null | undefined | ||||
|   chronologyItems: NonNullable< | ||||
|     GetChronologyItemsQuery["chronologyItems"] | ||||
|   >["data"]; | ||||
|   chronologyEras: NonNullable<GetErasQuery["chronologyEras"]>["data"]; | ||||
| } | ||||
| 
 | ||||
| export default function Chronology(props: Props): JSX.Element { | ||||
|  | ||||
| @ -1,13 +1,14 @@ | ||||
| import AppLayout from "components/AppLayout"; | ||||
| import NavOption from "components/PanelComponents/NavOption"; | ||||
| import PanelHeader from "components/PanelComponents/PanelHeader"; | ||||
| import SubPanel from "components/Panels/SubPanel"; | ||||
| import { AppLayout } from "components/AppLayout"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| import { PanelHeader } from "components/PanelComponents/PanelHeader"; | ||||
| import { SubPanel } from "components/Panels/SubPanel"; | ||||
| import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; | ||||
| import { Immutable } from "helpers/types"; | ||||
| import { GetStaticPropsContext } from "next"; | ||||
| import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; | ||||
| 
 | ||||
| interface Props extends AppStaticProps {} | ||||
| 
 | ||||
| export default function Wiki(props: Props): JSX.Element { | ||||
| export default function Wiki(props: Immutable<Props>): JSX.Element { | ||||
|   const { langui } = props; | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|  | ||||
| @ -159,5 +159,13 @@ module.exports = { | ||||
|         }, | ||||
|       }); | ||||
|     }), | ||||
| 
 | ||||
|     plugin(function ({ addUtilities }) { | ||||
|       addUtilities({ | ||||
|         ".break-words": { | ||||
|           "word-break": "break-word", | ||||
|         }, | ||||
|       }); | ||||
|     }), | ||||
|   ], | ||||
| }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint