Fix subpanel closing on mobile+ improvements
This commit is contained in:
		
							parent
							
								
									fe52ded606
								
							
						
					
					
						commit
						df8a7f820d
					
				
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -1,4 +1,5 @@ | ||||
| { | ||||
|   "css.lint.unknownAtRules": "ignore", | ||||
|   "editor.rulers": [100] | ||||
|   "editor.rulers": [100], | ||||
|   "typescript.preferences.importModuleSpecifier": "non-relative" | ||||
| } | ||||
|  | ||||
							
								
								
									
										42
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										42
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -16,7 +16,7 @@ | ||||
|         "cuid": "^2.1.8", | ||||
|         "intl-messageformat": "^10.3.0", | ||||
|         "isomorphic-dompurify": "^0.26.0", | ||||
|         "jotai": "^2.0.0", | ||||
|         "jotai": "^2.0.1", | ||||
|         "markdown-to-jsx": "^7.1.9", | ||||
|         "marked": "^4.2.12", | ||||
|         "material-symbols": "^0.4.4", | ||||
| @ -35,7 +35,7 @@ | ||||
|         "turndown": "^7.1.1", | ||||
|         "ua-parser-js": "^1.0.33", | ||||
|         "usehooks-ts": "^2.9.1", | ||||
|         "zod": "^3.20.5" | ||||
|         "zod": "^3.20.6" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@digitak/esrun": "^3.2.19", | ||||
| @ -60,7 +60,7 @@ | ||||
|         "eslint-plugin-import": "^2.27.5", | ||||
|         "graphql": "^16.6.0", | ||||
|         "graphql-request": "^5.1.0", | ||||
|         "next-sitemap": "^3.1.50", | ||||
|         "next-sitemap": "^3.1.52", | ||||
|         "prettier": "^2.8.4", | ||||
|         "prettier-plugin-tailwindcss": "^0.2.2", | ||||
|         "tailwindcss": "^3.2.6", | ||||
| @ -7496,9 +7496,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jotai": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.0.0.tgz", | ||||
|       "integrity": "sha512-04G0CRZQgp3xrFAezd6X14psZ2TRGekHeYMBcbDJ/BR8ZJQPS+j0YkMTxUxyG58HJnN2+adfj5sWQWoqgtp1XQ==", | ||||
|       "version": "2.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.0.1.tgz", | ||||
|       "integrity": "sha512-b/BpBFkv3nq8HgT6YX5h5/y9VfKIn9OL1dO6gd9bWTgKt6LLe24VIMURTDwSYS888XfubuRQlbepb5IQGAtmcQ==", | ||||
|       "engines": { | ||||
|         "node": ">=12.20.0" | ||||
|       }, | ||||
| @ -8150,9 +8150,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/next-sitemap": { | ||||
|       "version": "3.1.50", | ||||
|       "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-3.1.50.tgz", | ||||
|       "integrity": "sha512-BnxAbjOK1zVcYvpZ4sYfhPXcL3ajLh/AIJLR39YKrhFxrD92KkiAGuVaKhfpoQLUf+ldsGBkGpdml2N5Qdd1KA==", | ||||
|       "version": "3.1.52", | ||||
|       "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-3.1.52.tgz", | ||||
|       "integrity": "sha512-tY469i4QRV1PwM9BoL+HdKYBCJ83IQl3PmUNapG/Hxp0MIYIw1hINU8E+Edf5Kr8vHXfVzPqDoul/Abu2P0vkw==", | ||||
|       "dev": true, | ||||
|       "funding": [ | ||||
|         { | ||||
| @ -10741,9 +10741,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/zod": { | ||||
|       "version": "3.20.5", | ||||
|       "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.5.tgz", | ||||
|       "integrity": "sha512-BTAAliwfoB9dWf2hC+TXlyWKk/YTqRGZjHQR0WLC2A2pzierWo7KuQ1ebjS4SNaFaxg/lDItzl9/QTgLjcHbgw==", | ||||
|       "version": "3.20.6", | ||||
|       "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.6.tgz", | ||||
|       "integrity": "sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==", | ||||
|       "funding": { | ||||
|         "url": "https://github.com/sponsors/colinhacks" | ||||
|       } | ||||
| @ -16385,9 +16385,9 @@ | ||||
|       "requires": {} | ||||
|     }, | ||||
|     "jotai": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.0.0.tgz", | ||||
|       "integrity": "sha512-04G0CRZQgp3xrFAezd6X14psZ2TRGekHeYMBcbDJ/BR8ZJQPS+j0YkMTxUxyG58HJnN2+adfj5sWQWoqgtp1XQ==", | ||||
|       "version": "2.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.0.1.tgz", | ||||
|       "integrity": "sha512-b/BpBFkv3nq8HgT6YX5h5/y9VfKIn9OL1dO6gd9bWTgKt6LLe24VIMURTDwSYS888XfubuRQlbepb5IQGAtmcQ==", | ||||
|       "requires": {} | ||||
|     }, | ||||
|     "js-sdsl": { | ||||
| @ -16871,9 +16871,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "next-sitemap": { | ||||
|       "version": "3.1.50", | ||||
|       "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-3.1.50.tgz", | ||||
|       "integrity": "sha512-BnxAbjOK1zVcYvpZ4sYfhPXcL3ajLh/AIJLR39YKrhFxrD92KkiAGuVaKhfpoQLUf+ldsGBkGpdml2N5Qdd1KA==", | ||||
|       "version": "3.1.52", | ||||
|       "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-3.1.52.tgz", | ||||
|       "integrity": "sha512-tY469i4QRV1PwM9BoL+HdKYBCJ83IQl3PmUNapG/Hxp0MIYIw1hINU8E+Edf5Kr8vHXfVzPqDoul/Abu2P0vkw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@corex/deepmerge": "^4.0.29", | ||||
| @ -18719,9 +18719,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "zod": { | ||||
|       "version": "3.20.5", | ||||
|       "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.5.tgz", | ||||
|       "integrity": "sha512-BTAAliwfoB9dWf2hC+TXlyWKk/YTqRGZjHQR0WLC2A2pzierWo7KuQ1ebjS4SNaFaxg/lDItzl9/QTgLjcHbgw==" | ||||
|       "version": "3.20.6", | ||||
|       "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.6.tgz", | ||||
|       "integrity": "sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -28,7 +28,7 @@ | ||||
|     "cuid": "^2.1.8", | ||||
|     "intl-messageformat": "^10.3.0", | ||||
|     "isomorphic-dompurify": "^0.26.0", | ||||
|     "jotai": "^2.0.0", | ||||
|     "jotai": "^2.0.1", | ||||
|     "markdown-to-jsx": "^7.1.9", | ||||
|     "marked": "^4.2.12", | ||||
|     "material-symbols": "^0.4.4", | ||||
| @ -47,7 +47,7 @@ | ||||
|     "turndown": "^7.1.1", | ||||
|     "ua-parser-js": "^1.0.33", | ||||
|     "usehooks-ts": "^2.9.1", | ||||
|     "zod": "^3.20.5" | ||||
|     "zod": "^3.20.6" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@digitak/esrun": "^3.2.19", | ||||
| @ -72,7 +72,7 @@ | ||||
|     "eslint-plugin-import": "^2.27.5", | ||||
|     "graphql": "^16.6.0", | ||||
|     "graphql-request": "^5.1.0", | ||||
|     "next-sitemap": "^3.1.50", | ||||
|     "next-sitemap": "^3.1.52", | ||||
|     "prettier": "^2.8.4", | ||||
|     "prettier-plugin-tailwindcss": "^0.2.2", | ||||
|     "tailwindcss": "^3.2.6", | ||||
|  | ||||
| @ -77,15 +77,19 @@ export const AppLayout = ({ | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   const turnSubIntoContent = isDefined(subPanel) && isUndefined(contentPanel); | ||||
|   const turnSubIntoContent = isDefined(subPanel) && isUndefined(contentPanel) && is1ColumnLayout; | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       {...handlers} | ||||
|       id={Ids.Body} | ||||
|       className={cJoin( | ||||
|         "fixed inset-0 m-0 grid touch-pan-y bg-light p-0 [grid-template-areas:'main_sub_content']", | ||||
|         cIf(is1ColumnLayout, "grid-rows-[1fr_5rem] [grid-template-areas:'content''navbar']") | ||||
|         "fixed inset-0 m-0 grid touch-pan-y bg-light p-0", | ||||
|         cIf( | ||||
|           is1ColumnLayout, | ||||
|           "grid-rows-[1fr_5rem] [grid-template-areas:'content''navbar']", | ||||
|           "[grid-template-areas:'main_sub_content']" | ||||
|         ) | ||||
|       )} | ||||
|       style={{ | ||||
|         gridTemplateColumns: is1ColumnLayout | ||||
| @ -113,6 +117,22 @@ export const AppLayout = ({ | ||||
|         <meta property="og:image:type" content="image/jpeg" /> | ||||
|       </Head> | ||||
| 
 | ||||
|       {/* Content panel */} | ||||
|       <div | ||||
|         id={Ids.ContentPanel} | ||||
|         className={cJoin( | ||||
|           "bg-light texture-paper-dots [grid-area:content]", | ||||
|           cIf(contentPanelScroolbar, "overflow-y-scroll") | ||||
|         )}> | ||||
|         {isDefined(contentPanel) ? ( | ||||
|           contentPanel | ||||
|         ) : turnSubIntoContent ? ( | ||||
|           subPanel | ||||
|         ) : ( | ||||
|           <ContentPlaceholder message={format("select_option_sidebar")} icon={"chevron_left"} /> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       {/* Background when navbar is opened */} | ||||
|       <div | ||||
|         className={cJoin( | ||||
| @ -120,7 +140,7 @@ export const AppLayout = ({ | ||||
|             [grid-area:content]`,
 | ||||
|           cIf( | ||||
|             (isMainPanelOpened || isSubPanelOpened) && is1ColumnLayout, | ||||
|             "z-10 backdrop-blur", | ||||
|             "backdrop-blur", | ||||
|             "pointer-events-none touch-none" | ||||
|           ) | ||||
|         )}> | ||||
| @ -140,57 +160,11 @@ export const AppLayout = ({ | ||||
|         /> | ||||
|       </div> | ||||
| 
 | ||||
|       {/* Content panel */} | ||||
|       <div | ||||
|         id={Ids.ContentPanel} | ||||
|         className={cJoin( | ||||
|           "bg-light texture-paper-dots [grid-area:content]", | ||||
|           cIf(contentPanelScroolbar, "overflow-y-scroll") | ||||
|         )}> | ||||
|         {isDefined(contentPanel) ? ( | ||||
|           contentPanel | ||||
|         ) : ( | ||||
|           <ContentPlaceholder message={format("select_option_sidebar")} icon={"chevron_left"} /> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       {/* Sub panel */} | ||||
|       {isDefined(subPanel) && ( | ||||
|         <div | ||||
|           id={Ids.SubPanel} | ||||
|           className={cJoin( | ||||
|             `z-20 overflow-y-scroll border-r border-dark/50 bg-light
 | ||||
|               transition-transform duration-300 scrollbar-none texture-paper-dots`,
 | ||||
|             cIf( | ||||
|               is1ColumnLayout, | ||||
|               "justify-self-end border-r-0 [grid-area:content]", | ||||
|               "[grid-area:sub]" | ||||
|             ), | ||||
|             cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)] border-l"), | ||||
|             cIf(is1ColumnLayout && !isSubPanelOpened && !turnSubIntoContent, "translate-x-[100vw]"), | ||||
|             cIf(is1ColumnLayout && turnSubIntoContent, "w-full border-l-0") | ||||
|           )}> | ||||
|           {subPanel} | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {/* Main panel */} | ||||
|       <div | ||||
|         className={cJoin( | ||||
|           `z-30 overflow-y-scroll border-r border-dark/50 bg-light
 | ||||
|             transition-transform duration-300 scrollbar-none texture-paper-dots`,
 | ||||
|           cIf(is1ColumnLayout, "justify-self-start [grid-area:content]", "[grid-area:main]"), | ||||
|           cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)]"), | ||||
|           cIf(!isMainPanelOpened && is1ColumnLayout, "-translate-x-full") | ||||
|         )}> | ||||
|         <MainPanel /> | ||||
|       </div> | ||||
| 
 | ||||
|       {/* Navbar */} | ||||
|       <div | ||||
|         className={cJoin( | ||||
|           `z-10 grid grid-cols-[5rem_1fr_5rem] place-items-center border-t
 | ||||
|             border-dotted border-black bg-light texture-paper-dots [grid-area:navbar]`,
 | ||||
|           `z-40 grid grid-cols-[5rem_1fr_5rem] place-items-center border-t
 | ||||
|           border-dotted border-black bg-light texture-paper-dots [grid-area:navbar]`,
 | ||||
|           cIf(!is1ColumnLayout, "hidden") | ||||
|         )}> | ||||
|         <Ico | ||||
| @ -221,6 +195,37 @@ export const AppLayout = ({ | ||||
|           /> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       {/* Sub panel */} | ||||
|       {isDefined(subPanel) && !turnSubIntoContent && ( | ||||
|         <div | ||||
|           id={Ids.SubPanel} | ||||
|           className={cJoin( | ||||
|             `z-40 overflow-y-scroll border-r border-dark/50 bg-light
 | ||||
|               transition-transform duration-300 scrollbar-none texture-paper-dots`,
 | ||||
|             cIf( | ||||
|               is1ColumnLayout, | ||||
|               "justify-self-end border-r-0 [grid-area:content]", | ||||
|               "[grid-area:sub]" | ||||
|             ), | ||||
|             cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)] border-l"), | ||||
|             cIf(is1ColumnLayout && !isSubPanelOpened, "translate-x-[100vw]") | ||||
|           )}> | ||||
|           {subPanel} | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {/* Main panel */} | ||||
|       <div | ||||
|         className={cJoin( | ||||
|           `z-40 overflow-y-scroll border-r border-dark/50 bg-light
 | ||||
|             transition-transform duration-300 scrollbar-none texture-paper-dots`,
 | ||||
|           cIf(is1ColumnLayout, "justify-self-start [grid-area:content]", "[grid-area:main]"), | ||||
|           cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)]"), | ||||
|           cIf(!isMainPanelOpened && is1ColumnLayout, "-translate-x-full") | ||||
|         )}> | ||||
|         <MainPanel /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { useCallback } from "react"; | ||||
| import { MouseEventHandler, useCallback } from "react"; | ||||
| import { DatePickerFragment } from "graphql/generated"; | ||||
| import { TranslatedProps } from "types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| @ -17,12 +17,21 @@ interface Props { | ||||
|   url: string; | ||||
|   active?: boolean; | ||||
|   disabled?: boolean; | ||||
|   onClick?: MouseEventHandler<HTMLAnchorElement>; | ||||
| } | ||||
| 
 | ||||
| export const ChroniclePreview = ({ date, url, title, active, disabled }: Props): JSX.Element => ( | ||||
| export const ChroniclePreview = ({ | ||||
|   date, | ||||
|   url, | ||||
|   title, | ||||
|   active, | ||||
|   disabled, | ||||
|   onClick, | ||||
| }: Props): JSX.Element => ( | ||||
|   <DownPressable | ||||
|     className="flex w-full gap-4 py-4 px-5" | ||||
|     href={url} | ||||
|     onClick={onClick} | ||||
|     active={active} | ||||
|     border | ||||
|     disabled={disabled}> | ||||
|  | ||||
| @ -8,6 +8,8 @@ import { Ico } from "components/Ico"; | ||||
| import { compareDate } from "helpers/date"; | ||||
| import { TranslatedProps } from "types/TranslatedProps"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { useAtomSetter } from "helpers/atoms"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -25,6 +27,7 @@ interface Props { | ||||
| } | ||||
| 
 | ||||
| const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element => { | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
|   const { value: isOpen, toggle: toggleOpen } = useBoolean( | ||||
|     chronicles.some((chronicle) => chronicle.attributes?.slug === currentSlug) | ||||
|   ); | ||||
| @ -75,6 +78,7 @@ const ChroniclesList = ({ chronicles, currentSlug, title }: Props): JSX.Element | ||||
|                         "/#chronicle-", | ||||
|                         chronicle.attributes.slug | ||||
|                       )} | ||||
|                       onClick={() => setSubPanelOpened(false)} | ||||
|                     /> | ||||
|                   )) | ||||
|                 : chronicle.attributes.translations.length > 0 && ( | ||||
|  | ||||
| @ -19,6 +19,12 @@ export enum ContentPanelWidthSizes { | ||||
|   Full = "full", | ||||
| } | ||||
| 
 | ||||
| const contentPanelWidthSizesToClassName: Record<ContentPanelWidthSizes, string> = { | ||||
|   default: "max-w-2xl", | ||||
|   large: "max-w-4xl", | ||||
|   full: "w-full", | ||||
| }; | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| 
 | ||||
| export const ContentPanel = ({ | ||||
| @ -31,13 +37,9 @@ export const ContentPanel = ({ | ||||
|     <div className="grid h-full"> | ||||
|       <main | ||||
|         className={cJoin( | ||||
|           "relative justify-self-center px-4 pt-10 pb-20", | ||||
|           cIf(isContentPanelAtLeast3xl, "px-10 pt-20 pb-32"), | ||||
|           width === ContentPanelWidthSizes.Default | ||||
|             ? "max-w-2xl" | ||||
|             : width === ContentPanelWidthSizes.Large | ||||
|             ? "max-w-4xl" | ||||
|             : "w-full", | ||||
|           "relative justify-self-center", | ||||
|           cIf(isContentPanelAtLeast3xl, "px-10 pt-20 pb-32", "px-4 pt-10 pb-20"), | ||||
|           contentPanelWidthSizesToClassName[width], | ||||
|           className | ||||
|         )}> | ||||
|         {children} | ||||
|  | ||||
| @ -14,7 +14,7 @@ interface Props { | ||||
|   children: React.ReactNode; | ||||
|   className?: string; | ||||
|   onFocusChanged?: (isFocused: boolean) => void; | ||||
|   onClick?: MouseEventHandler<HTMLDivElement>; | ||||
|   onClick?: MouseEventHandler<HTMLAnchorElement>; | ||||
| } | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { useState } from "react"; | ||||
| import { MouseEventHandler, useState } from "react"; | ||||
| import { Link } from "components/Inputs/Link"; | ||||
| import { cIf, cJoin } from "helpers/className"; | ||||
| 
 | ||||
| @ -8,6 +8,7 @@ interface Props { | ||||
|   className?: string; | ||||
|   noBackground?: boolean; | ||||
|   disabled?: boolean; | ||||
|   onClick?: MouseEventHandler<HTMLAnchorElement>; | ||||
| } | ||||
| 
 | ||||
| export const UpPressable = ({ | ||||
| @ -16,12 +17,14 @@ export const UpPressable = ({ | ||||
|   className, | ||||
|   disabled = false, | ||||
|   noBackground = false, | ||||
|   onClick, | ||||
| }: Props): JSX.Element => { | ||||
|   const [isFocused, setFocused] = useState(false); | ||||
|   return ( | ||||
|     <Link | ||||
|       href={href} | ||||
|       onFocusChanged={setFocused} | ||||
|       onClick={onClick} | ||||
|       className={cJoin( | ||||
|         `drop-shadow-lg transition-all duration-300 shadow-shade`, | ||||
|         cIf(!noBackground, "overflow-hidden rounded-md bg-highlight"), | ||||
|  | ||||
| @ -55,9 +55,9 @@ export const Button = ({ | ||||
|         onFocus={(event) => event.target.blur()} | ||||
|         className={cJoin( | ||||
|           `group grid cursor-pointer select-none grid-flow-col place-content-center 
 | ||||
|           place-items-center gap-2 rounded-full border border-dark py-3 px-4 | ||||
|           place-items-center gap-2 rounded-full border border-dark  | ||||
|           leading-none text-dark transition-all`,
 | ||||
|           cIf(size === "small", "px-3 py-1 text-xs"), | ||||
|           cIf(size === "small", "px-3 py-1 text-xs", "py-3 px-4"), | ||||
|           cIf(active, "!border-black bg-black !text-light drop-shadow-lg shadow-black"), | ||||
|           cIf( | ||||
|             disabled, | ||||
| @ -74,16 +74,16 @@ export const Button = ({ | ||||
|         {isDefined(badgeNumber) && ( | ||||
|           <div | ||||
|             className={cJoin( | ||||
|               `absolute -top-3 -right-2 grid h-8 w-8 place-items-center rounded-full bg-dark
 | ||||
|               `absolute grid place-items-center rounded-full bg-dark
 | ||||
|               font-bold text-light transition-opacity group-hover:opacity-0`,
 | ||||
|               cIf(size === "small", "-top-2 -right-2 h-5 w-5") | ||||
|               cIf(size === "small", "-top-2 -right-2 h-5 w-5", "-top-3 -right-2 h-8 w-8") | ||||
|             )}> | ||||
|             <p className="-translate-y-[0.05em]">{badgeNumber}</p> | ||||
|           </div> | ||||
|         )} | ||||
|         {isDefinedAndNotEmpty(icon) && ( | ||||
|           <Ico | ||||
|             className="[font-size:150%] [line-height:0.66]" | ||||
|             className="![font-size:150%] ![line-height:0.66]" | ||||
|             icon={icon} | ||||
|             isFilled={active} | ||||
|             opticalSize={size === "normal" ? 24 : 20} | ||||
|  | ||||
| @ -9,7 +9,7 @@ interface Props { | ||||
|   className?: string; | ||||
|   alwaysNewTab?: boolean; | ||||
|   children: React.ReactNode; | ||||
|   onClick?: MouseEventHandler<HTMLDivElement>; | ||||
|   onClick?: MouseEventHandler<HTMLAnchorElement>; | ||||
|   onFocusChanged?: (isFocused: boolean) => void; | ||||
|   disabled?: boolean; | ||||
|   linkStyled?: boolean; | ||||
| @ -22,6 +22,7 @@ export const Link = ({ | ||||
|   alwaysNewTab, | ||||
|   disabled, | ||||
|   linkStyled = false, | ||||
|   onClick, | ||||
|   onFocusChanged, | ||||
| }: Props): JSX.Element => ( | ||||
|   <ConditionalWrapper | ||||
| @ -29,6 +30,7 @@ export const Link = ({ | ||||
|     wrapperProps={{ | ||||
|       href: href ?? "", | ||||
|       alwaysNewTab, | ||||
|       onClick, | ||||
|       onFocusChanged, | ||||
|       className: cJoin( | ||||
|         cIf( | ||||
| @ -51,12 +53,14 @@ interface LinkWrapperProps { | ||||
|   className?: string; | ||||
|   alwaysNewTab?: boolean; | ||||
|   onFocusChanged?: (isFocused: boolean) => void; | ||||
|   onClick?: MouseEventHandler<HTMLAnchorElement>; | ||||
| } | ||||
| 
 | ||||
| const LinkWrapper = ({ | ||||
|   children, | ||||
|   className, | ||||
|   onFocusChanged, | ||||
|   onClick, | ||||
|   alwaysNewTab = false, | ||||
|   href, | ||||
| }: LinkWrapperProps & Wrapper) => ( | ||||
| @ -65,6 +69,7 @@ const LinkWrapper = ({ | ||||
|     className={className} | ||||
|     target={alwaysNewTab ? "_blank" : "_self"} | ||||
|     replace={href.startsWith("#")} | ||||
|     onClick={onClick} | ||||
|     onMouseLeave={() => onFocusChanged?.(false)} | ||||
|     onMouseDown={() => onFocusChanged?.(true)} | ||||
|     onMouseUp={() => onFocusChanged?.(false)}> | ||||
|  | ||||
| @ -58,13 +58,12 @@ export const Select = ({ | ||||
|       <div | ||||
|         className={cJoin( | ||||
|           `grid cursor-pointer select-none grid-flow-col grid-cols-[1fr_auto_auto]
 | ||||
|            place-items-center rounded-3xl p-1 outline outline-1 -outline-offset-1 | ||||
|            outline-mid`,
 | ||||
|            place-items-center rounded-3xl p-1 outline outline-1 -outline-offset-1`,
 | ||||
|           cIf(isOpened, "rounded-b-none bg-highlight outline-transparent"), | ||||
|           cIf( | ||||
|             disabled, | ||||
|             "cursor-not-allowed text-dark opacity-50 outline-dark/60 grayscale", | ||||
|             "transition-all hover:bg-mid hover:outline-transparent" | ||||
|             "outline-mid transition-all hover:bg-mid hover:outline-transparent" | ||||
|           ) | ||||
|         )}> | ||||
|         <p onClick={tryToggling} className="w-full px-4 py-1"> | ||||
|  | ||||
| @ -21,10 +21,14 @@ export const Switch = ({ value, onClick, className, disabled = false }: Props): | ||||
|     <div | ||||
|       className={cJoin( | ||||
|         `relative grid h-6 w-12 content-center rounded-full border-mid outline
 | ||||
|         outline-1 -outline-offset-1 outline-mid transition-colors`,
 | ||||
|         cIf(value, "border-none bg-mid shadow-inner-sm outline-transparent shadow-shade"), | ||||
|         cIf(disabled, "cursor-not-allowed opacity-50 grayscale", "cursor-pointer"), | ||||
|         cIf(disabled, cIf(value, "bg-dark/40 outline-transparent", "outline-dark/60")), | ||||
|         outline-1 -outline-offset-1 transition-colors`,
 | ||||
|         cIf(value, "border-none shadow-inner-sm shadow-shade"), | ||||
|         cIf(disabled, "cursor-not-allowed opacity-50 grayscale", "cursor-pointer outline-mid"), | ||||
|         cIf( | ||||
|           disabled, | ||||
|           cIf(value, "bg-dark/40 outline-transparent", "outline-dark/60"), | ||||
|           cIf(value, "bg-mid outline-transparent") | ||||
|         ), | ||||
|         className | ||||
|       )} | ||||
|       onClick={() => { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import Markdown from "markdown-to-jsx"; | ||||
| import React, { Fragment, useMemo } from "react"; | ||||
| import React, { Fragment, MouseEventHandler, useMemo } from "react"; | ||||
| import ReactDOMServer from "react-dom/server"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Img } from "components/Img"; | ||||
| @ -218,13 +218,13 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme | ||||
| interface TableOfContentsProps { | ||||
|   text: string; | ||||
|   title?: string; | ||||
|   horizontalLine?: boolean; | ||||
|   onContentClicked?: MouseEventHandler<HTMLAnchorElement>; | ||||
| } | ||||
| 
 | ||||
| export const TableOfContents = ({ | ||||
|   text, | ||||
|   title, | ||||
|   horizontalLine = false, | ||||
|   onContentClicked, | ||||
| }: TableOfContentsProps): JSX.Element => { | ||||
|   const { format } = useFormat(); | ||||
|   const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title); | ||||
| @ -233,17 +233,20 @@ export const TableOfContents = ({ | ||||
|     <> | ||||
|       {toc.children.length > 0 && ( | ||||
|         <> | ||||
|           {horizontalLine && <HorizontalLine />} | ||||
|           <h3 className="text-xl">{format("table_of_contents")}</h3> | ||||
|           <div className="max-w-[14.5rem] text-left"> | ||||
|             <p | ||||
|               className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap | ||||
|                 text-left"> | ||||
|               <Link href={`#${toc.slug}`} linkStyled> | ||||
|               <Link href={`#${toc.slug}`} linkStyled onClick={onContentClicked}> | ||||
|                 {<abbr title={toc.title}>{toc.title}</abbr>} | ||||
|               </Link> | ||||
|             </p> | ||||
|             <TocLevel tocchildren={toc.children} parentNumbering="" /> | ||||
|             <TocLevel | ||||
|               tocchildren={toc.children} | ||||
|               parentNumbering="" | ||||
|               onContentClicked={onContentClicked} | ||||
|             /> | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
| @ -334,12 +337,14 @@ interface LevelProps { | ||||
|   tocchildren: TocInterface[]; | ||||
|   parentNumbering: string; | ||||
|   allowIntersection?: boolean; | ||||
|   onContentClicked?: MouseEventHandler<HTMLAnchorElement>; | ||||
| } | ||||
| 
 | ||||
| const TocLevel = ({ | ||||
|   tocchildren, | ||||
|   parentNumbering, | ||||
|   allowIntersection = true, | ||||
|   onContentClicked, | ||||
| }: LevelProps): JSX.Element => { | ||||
|   const ids = useMemo(() => tocchildren.map((child) => child.slug), [tocchildren]); | ||||
|   const currentIntersection = useIntersectionList(ids); | ||||
| @ -354,7 +359,7 @@ const TocLevel = ({ | ||||
|               cIf(allowIntersection && currentIntersection === childIndex, "text-dark") | ||||
|             )}> | ||||
|             <span className="text-dark">{`${parentNumbering}${childIndex + 1}.`}</span>{" "} | ||||
|             <Link href={`#${child.slug}`} linkStyled> | ||||
|             <Link href={`#${child.slug}`} linkStyled onClick={onContentClicked}> | ||||
|               {<abbr title={child.title}>{child.title}</abbr>} | ||||
|             </Link> | ||||
|           </li> | ||||
| @ -362,6 +367,7 @@ const TocLevel = ({ | ||||
|             tocchildren={child.children} | ||||
|             parentNumbering={`${parentNumbering}${childIndex + 1}.`} | ||||
|             allowIntersection={allowIntersection && currentIntersection === childIndex} | ||||
|             onContentClicked={onContentClicked} | ||||
|           /> | ||||
|         </Fragment> | ||||
|       ))} | ||||
|  | ||||
| @ -23,7 +23,7 @@ interface Props { | ||||
|   reduced?: boolean; | ||||
|   active?: boolean; | ||||
|   disabled?: boolean; | ||||
|   onClick?: MouseEventHandler<HTMLDivElement>; | ||||
|   onClick?: MouseEventHandler<HTMLAnchorElement>; | ||||
| } | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { useCallback } from "react"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Button } from "components/Inputs/Button"; | ||||
| import { NavOption } from "components/PanelComponents/NavOption"; | ||||
| @ -21,6 +22,8 @@ export const MainPanel = (): JSX.Element => { | ||||
|   const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout); | ||||
|   const { format } = useFormat(); | ||||
|   const [isMainPanelReduced, setMainPanelReduced] = useAtomPair(atoms.layout.mainPanelReduced); | ||||
|   const setMainPanelOpened = useAtomSetter(atoms.layout.mainPanelOpened); | ||||
|   const closeMainPanel = useCallback(() => setMainPanelOpened(false), [setMainPanelOpened]); | ||||
|   const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened); | ||||
|   const setSearchOpened = useAtomSetter(atoms.layout.searchOpened); | ||||
| 
 | ||||
| @ -53,7 +56,10 @@ export const MainPanel = (): JSX.Element => { | ||||
|       )} | ||||
|       <div> | ||||
|         <div className="grid place-items-center"> | ||||
|           <Link href="/" className="flex w-full cursor-pointer justify-center"> | ||||
|           <Link | ||||
|             href="/" | ||||
|             className="flex w-full cursor-pointer justify-center" | ||||
|             onClick={closeMainPanel}> | ||||
|             <ColoredSvg | ||||
|               src="/icons/accords.svg" | ||||
|               className={cJoin( | ||||
| @ -77,6 +83,7 @@ export const MainPanel = (): JSX.Element => { | ||||
|               placement={isMainPanelReduced ? "right" : "top"}> | ||||
|               <Button | ||||
|                 onClick={() => { | ||||
|                   closeMainPanel(); | ||||
|                   setSettingsOpened(true); | ||||
|                   sendAnalytics("Settings", "Open settings"); | ||||
|                 }} | ||||
| @ -88,6 +95,7 @@ export const MainPanel = (): JSX.Element => { | ||||
|               placement={isMainPanelReduced ? "right" : "top"}> | ||||
|               <Button | ||||
|                 onClick={() => { | ||||
|                   closeMainPanel(); | ||||
|                   setSearchOpened(true); | ||||
|                   sendAnalytics("Search", "Open search"); | ||||
|                 }} | ||||
| @ -106,6 +114,7 @@ export const MainPanel = (): JSX.Element => { | ||||
|         title={format("library")} | ||||
|         subtitle={format("library_short_description")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|         onClick={closeMainPanel} | ||||
|       /> | ||||
| 
 | ||||
|       <NavOption | ||||
| @ -114,6 +123,7 @@ export const MainPanel = (): JSX.Element => { | ||||
|         title={format("contents")} | ||||
|         subtitle={format("contents_short_description")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|         onClick={closeMainPanel} | ||||
|       /> | ||||
| 
 | ||||
|       <NavOption | ||||
| @ -122,6 +132,7 @@ export const MainPanel = (): JSX.Element => { | ||||
|         title={format("wiki")} | ||||
|         subtitle={format("wiki_short_description")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|         onClick={closeMainPanel} | ||||
|       /> | ||||
| 
 | ||||
|       <NavOption | ||||
| @ -130,6 +141,7 @@ export const MainPanel = (): JSX.Element => { | ||||
|         title={format("chronicles")} | ||||
|         subtitle={format("chronicles_short_description")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|         onClick={closeMainPanel} | ||||
|       /> | ||||
| 
 | ||||
|       <HorizontalLine /> | ||||
| @ -139,22 +151,15 @@ export const MainPanel = (): JSX.Element => { | ||||
|         icon="newspaper" | ||||
|         title={format("news")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|         onClick={closeMainPanel} | ||||
|       /> | ||||
| 
 | ||||
|       {/* | ||||
|       <NavOption | ||||
|         url="/merch" | ||||
|         icon="store" | ||||
|         title={format("merch")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|       /> | ||||
|       */} | ||||
| 
 | ||||
|       <NavOption | ||||
|         url="https://gallery.accords-library.com/posts/" | ||||
|         icon="perm_media" | ||||
|         title={format("gallery")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|         onClick={closeMainPanel} | ||||
|       /> | ||||
| 
 | ||||
|       <NavOption | ||||
| @ -162,6 +167,7 @@ export const MainPanel = (): JSX.Element => { | ||||
|         icon="save" | ||||
|         title={format("archives")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|         onClick={closeMainPanel} | ||||
|       /> | ||||
| 
 | ||||
|       <NavOption | ||||
| @ -169,6 +175,7 @@ export const MainPanel = (): JSX.Element => { | ||||
|         icon="info" | ||||
|         title={format("about_us")} | ||||
|         reduced={isMainPanelReduced && is3ColumnsLayout} | ||||
|         onClick={closeMainPanel} | ||||
|       /> | ||||
| 
 | ||||
|       {(!isMainPanelReduced || !is3ColumnsLayout) && <HorizontalLine />} | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { MaterialSymbol } from "material-symbols"; | ||||
| import { Popup } from "components/Containers/Popup"; | ||||
| import { sendAnalytics } from "helpers/analytics"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomPair } from "helpers/atoms"; | ||||
| import { useAtomPair, useAtomSetter } from "helpers/atoms"; | ||||
| import { TextInput } from "components/Inputs/TextInput"; | ||||
| import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search"; | ||||
| import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard"; | ||||
| @ -197,6 +197,7 @@ export const SearchPopup = (): JSX.Element => { | ||||
|                   key={item.id} | ||||
|                   className="w-56" | ||||
|                   href={`/library/${item.slug}`} | ||||
|                   onClick={() => setSearchOpened(false)} | ||||
|                   translations={filterHasAttributes(item._formatted.descriptions, [ | ||||
|                     "language.data.attributes.code", | ||||
|                   ] as const).map((translation) => ({ | ||||
| @ -243,6 +244,7 @@ export const SearchPopup = (): JSX.Element => { | ||||
|                   key={item.id} | ||||
|                   className="w-56" | ||||
|                   href={`/contents/${item.slug}`} | ||||
|                   onClick={() => setSearchOpened(false)} | ||||
|                   translations={filterHasAttributes(item._formatted.translations, [ | ||||
|                     "language.data.attributes.code", | ||||
|                   ] as const).map(({ displayable_description, language, ...otherAttributes }) => ({ | ||||
| @ -287,6 +289,7 @@ export const SearchPopup = (): JSX.Element => { | ||||
|                   key={item.id} | ||||
|                   className="w-56" | ||||
|                   href={`/wiki/${item.slug}`} | ||||
|                   onClick={() => setSearchOpened(false)} | ||||
|                   translations={filterHasAttributes(item._formatted.translations, [ | ||||
|                     "language.data.attributes.code", | ||||
|                   ] as const).map( | ||||
| @ -338,6 +341,7 @@ export const SearchPopup = (): JSX.Element => { | ||||
|                   className="w-56" | ||||
|                   key={item.id} | ||||
|                   href={`/news/${item.slug}`} | ||||
|                   onClick={() => setSearchOpened(false)} | ||||
|                   translations={filterHasAttributes(item._formatted.translations, [ | ||||
|                     "language.data.attributes.code", | ||||
|                   ] as const).map(({ excerpt, body, language, ...otherAttributes }) => ({ | ||||
| @ -380,6 +384,7 @@ export const SearchPopup = (): JSX.Element => { | ||||
|                   className="w-56" | ||||
|                   key={item.uid} | ||||
|                   href={`/archives/videos/v/${item.uid}`} | ||||
|                   onClick={() => setSearchOpened(false)} | ||||
|                   title={item._formatted.title} | ||||
|                   thumbnail={getVideoThumbnailURL(item.uid)} | ||||
|                   thumbnailAspectRatio="16/9" | ||||
| @ -427,6 +432,7 @@ const SearchResultSection = ({ | ||||
|   children, | ||||
| }: SearchResultSectionProps) => { | ||||
|   const { format } = useFormat(); | ||||
|   const setSearchOpened = useAtomSetter(atoms.layout.searchOpened); | ||||
|   return ( | ||||
|     <> | ||||
|       {isDefined(totalHits) && totalHits > 0 && ( | ||||
| @ -434,7 +440,8 @@ const SearchResultSection = ({ | ||||
|           <div className="mb-6 grid place-content-start"> | ||||
|             <UpPressable | ||||
|               className="grid grid-cols-[auto_1fr] place-items-center gap-6 px-6 py-4" | ||||
|               href={href}> | ||||
|               href={href} | ||||
|               onClick={() => setSearchOpened(false)}> | ||||
|               <Ico icon={icon} className="!text-3xl" isFilled /> | ||||
|               <div> | ||||
|                 <p className="font-headers text-lg">{title}</p> | ||||
|  | ||||
| @ -11,9 +11,12 @@ import { ThumbnailHeader } from "./ThumbnailHeader"; | ||||
| import { ToolTip } from "./ToolTip"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { PostWithTranslations } from "types/types"; | ||||
| import { filterHasAttributes } from "helpers/asserts"; | ||||
| import { filterHasAttributes, isDefined } from "helpers/asserts"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { useAtomGetter, useAtomSetter } from "helpers/atoms"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { ElementsSeparator } from "helpers/component"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -49,6 +52,8 @@ export const PostPage = ({ | ||||
|   ...otherProps | ||||
| }: Props): JSX.Element => { | ||||
|   const { format, formatStatusDescription } = useFormat(); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
|   const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout); | ||||
| 
 | ||||
|   const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({ | ||||
|     items: post.translations, | ||||
| @ -65,49 +70,56 @@ export const PostPage = ({ | ||||
|   const title = selectedTranslation?.title ?? prettySlug(post.slug); | ||||
|   const excerpt = selectedTranslation?.excerpt ?? ""; | ||||
| 
 | ||||
|   const subPanel = | ||||
|     returnHref || returnTitle || displayCredits || displayToc ? ( | ||||
|       <SubPanel> | ||||
|         {returnHref && returnTitle && ( | ||||
|           <ReturnButton href={returnHref} title={returnTitle} displayOnlyOn={"3ColumnsLayout"} /> | ||||
|         )} | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <ElementsSeparator> | ||||
|         {[ | ||||
|           returnHref && returnTitle && !is1ColumnLayout && ( | ||||
|             <ReturnButton href={returnHref} title={returnTitle} /> | ||||
|           ), | ||||
| 
 | ||||
|         {displayCredits && ( | ||||
|           <> | ||||
|             <HorizontalLine /> | ||||
|           displayCredits && ( | ||||
|             <> | ||||
|               {selectedTranslation && ( | ||||
|                 <div className="grid grid-flow-col place-content-center place-items-center gap-2"> | ||||
|                   <p className="font-headers font-bold">{format("status")}:</p> | ||||
| 
 | ||||
|             {selectedTranslation && ( | ||||
|               <div className="grid grid-flow-col place-content-center place-items-center gap-2"> | ||||
|                 <p className="font-headers font-bold">{format("status")}:</p> | ||||
| 
 | ||||
|                 <ToolTip | ||||
|                   content={formatStatusDescription(selectedTranslation.status)} | ||||
|                   maxWidth={"20rem"}> | ||||
|                   <Chip text={selectedTranslation.status} /> | ||||
|                 </ToolTip> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {post.authors && post.authors.data.length > 0 && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{"Authors"}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   {filterHasAttributes(post.authors.data, ["id", "attributes"] as const).map( | ||||
|                     (author) => ( | ||||
|                       <Fragment key={author.id}> | ||||
|                         <RecorderChip recorder={author.attributes} /> | ||||
|                       </Fragment> | ||||
|                     ) | ||||
|                   )} | ||||
|                   <ToolTip | ||||
|                     content={formatStatusDescription(selectedTranslation.status)} | ||||
|                     maxWidth={"20rem"}> | ||||
|                     <Chip text={selectedTranslation.status} /> | ||||
|                   </ToolTip> | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
|           </> | ||||
|         )} | ||||
|               )} | ||||
| 
 | ||||
|         {displayToc && <TableOfContents text={body} title={title} horizontalLine />} | ||||
|       </SubPanel> | ||||
|     ) : undefined; | ||||
|               {post.authors && post.authors.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold">{"Authors"}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(post.authors.data, ["id", "attributes"] as const).map( | ||||
|                       (author) => ( | ||||
|                         <Fragment key={author.id}> | ||||
|                           <RecorderChip recorder={author.attributes} /> | ||||
|                         </Fragment> | ||||
|                       ) | ||||
|                     )} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
|             </> | ||||
|           ), | ||||
| 
 | ||||
|           displayToc && ( | ||||
|             <TableOfContents | ||||
|               text={body} | ||||
|               title={title} | ||||
|               onContentClicked={() => setSubPanelOpened(false)} | ||||
|             /> | ||||
|           ), | ||||
|         ]} | ||||
|       </ElementsSeparator> | ||||
|     </SubPanel> | ||||
|   ); | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel> | ||||
| @ -133,6 +145,7 @@ export const PostPage = ({ | ||||
|               ) : undefined | ||||
|             } | ||||
|           /> | ||||
|           {(isDefined(prependBody) || isDefined(body)) && <HorizontalLine />} | ||||
|         </> | ||||
|       ) : ( | ||||
|         <> | ||||
| @ -148,12 +161,7 @@ export const PostPage = ({ | ||||
|       )} | ||||
| 
 | ||||
|       {prependBody} | ||||
|       {body && ( | ||||
|         <> | ||||
|           {displayThumbnailHeader && <HorizontalLine />} | ||||
|           <Markdawn text={body} /> | ||||
|         </> | ||||
|       )} | ||||
|       {body && <Markdawn text={body} />} | ||||
| 
 | ||||
|       {appendBody} | ||||
|     </ContentPanel> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { useCallback } from "react"; | ||||
| import { MouseEventHandler, useCallback } from "react"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { Markdown } from "./Markdown/Markdown"; | ||||
| import { Chip } from "components/Chip"; | ||||
| @ -50,6 +50,7 @@ interface Props { | ||||
|     | { __typename: "anotherHoverlayName" }; | ||||
|   disabled?: boolean; | ||||
|   className?: string; | ||||
|   onClick?: MouseEventHandler<HTMLAnchorElement>; | ||||
| } | ||||
| 
 | ||||
| // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
 | ||||
| @ -72,6 +73,7 @@ export const PreviewCard = ({ | ||||
|   infoAppend, | ||||
|   className, | ||||
|   disabled = false, | ||||
|   onClick, | ||||
| }: Props): JSX.Element => { | ||||
|   const currency = useAtomGetter(atoms.settings.currency); | ||||
|   const currencies = useAtomGetter(atoms.localData.currencies); | ||||
| @ -115,6 +117,7 @@ export const PreviewCard = ({ | ||||
|     <UpPressable | ||||
|       className={cJoin("grid items-end text-left", className)} | ||||
|       href={href} | ||||
|       onClick={onClick} | ||||
|       noBackground | ||||
|       disabled={disabled}> | ||||
|       <div className={cJoin("group", cIf(disabled, "pointer-events-none touch-none select-none"))}> | ||||
| @ -139,17 +142,15 @@ export const PreviewCard = ({ | ||||
|             {hoverlay && hoverlay.__typename === "Video" && ( | ||||
|               <> | ||||
|                 <div | ||||
|                   className="absolute inset-0 grid place-content-center bg-shade/0 | ||||
|                    text-light transition-colors group-hover:bg-shade/50"> | ||||
|                   className="absolute inset-0 grid place-content-center rounded-t-md | ||||
|                    bg-shade/0 text-light transition-colors group-hover:bg-shade/50"> | ||||
|                   <Ico | ||||
|                     icon="play_circle" | ||||
|                     className="!text-6xl text-light opacity-0 drop-shadow-lg transition-opacity | ||||
|                     shadow-shade group-hover:opacity-100 dark:text-black" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <div | ||||
|                   className="absolute right-2 bottom-2 rounded-full bg-black/60 px-2 | ||||
|                   text-light"> | ||||
|                 <div className="absolute right-2 bottom-2 rounded-full bg-black/60 px-2 text-light"> | ||||
|                   {prettyDuration(hoverlay.duration)} | ||||
|                 </div> | ||||
|               </> | ||||
| @ -166,14 +167,14 @@ export const PreviewCard = ({ | ||||
|         )} | ||||
|         <div | ||||
|           className={cJoin( | ||||
|             "z-20 grid gap-2 p-4 transition-opacity linearbg-obi", | ||||
|             "z-20 gap-2 p-4 transition-opacity linearbg-obi", | ||||
|             cIf( | ||||
|               !keepInfoVisible && isHoverable, | ||||
|               `-inset-x-0.5 bottom-2 opacity-0 shadow-shade
 | ||||
|                [border-radius:10%_10%_10%_10%_/_1%_1%_3%_3%] | ||||
|                group-hover:opacity-100 hoverable:absolute hoverable:drop-shadow-lg | ||||
|                notHoverable:rounded-b-md notHoverable:opacity-100`,
 | ||||
|               "[border-radius:0%_0%_10%_10%_/_0%_0%_3%_3%]" | ||||
|               "grid [border-radius:0%_0%_10%_10%_/_0%_0%_3%_3%]" | ||||
|             ) | ||||
|           )}> | ||||
|           {metadata?.position === "Top" && metadataJSX} | ||||
|  | ||||
| @ -1,28 +1,19 @@ | ||||
| import React, { useCallback, useState } from "react"; | ||||
| import React, { useCallback, useEffect, useState } from "react"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useEffectOnce } from "usehooks-ts"; | ||||
| import { atom } from "jotai"; | ||||
| import { UploadImageFragment } from "graphql/generated"; | ||||
| import { LightBox } from "components/LightBox"; | ||||
| import { filterDefined } from "helpers/asserts"; | ||||
| import { atomPairing, useAtomSetter } from "helpers/atoms"; | ||||
| 
 | ||||
| const lightBoxAtom = atomPairing( | ||||
|   atom<{ | ||||
|     showLightBox: ( | ||||
|       images: (UploadImageFragment | string | null | undefined)[], | ||||
|       index?: number | ||||
|     ) => void; | ||||
|   }>({ showLightBox: () => null }) | ||||
| ); | ||||
| 
 | ||||
| export const lightBox = lightBoxAtom[0]; | ||||
| import { useAtomSetter } from "helpers/atoms"; | ||||
| import { internalAtoms } from "contexts/atoms"; | ||||
| 
 | ||||
| export const LightBoxProvider = (): JSX.Element => { | ||||
|   const router = useRouter(); | ||||
|   const [isLightBoxVisible, setLightBoxVisibility] = useState(false); | ||||
|   const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]); | ||||
|   const [lightBoxIndex, setLightBoxIndex] = useState(0); | ||||
| 
 | ||||
|   const setShowLightBox = useAtomSetter(lightBoxAtom); | ||||
|   const setShowLightBox = useAtomSetter(internalAtoms.lightBox); | ||||
| 
 | ||||
|   useEffectOnce(() => | ||||
|     setShowLightBox({ | ||||
| @ -40,6 +31,8 @@ export const LightBoxProvider = (): JSX.Element => { | ||||
|     setTimeout(() => setLightBoxImages([]), 100); | ||||
|   }, []); | ||||
| 
 | ||||
|   useEffect(() => router.events.on("routeChangeStart", closeLightBox)); | ||||
| 
 | ||||
|   return ( | ||||
|     <LightBox | ||||
|       isVisible={isLightBoxVisible} | ||||
|  | ||||
| @ -1,31 +0,0 @@ | ||||
| import { useRouter } from "next/router"; | ||||
| import { useEffect } from "react"; | ||||
| import { useScrollIntoView } from "hooks/useScrollIntoView"; | ||||
| import { useAtomSetter } from "helpers/atoms"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| 
 | ||||
| export const useAppLayout = (): void => { | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   const setSearchOpened = useAtomSetter(atoms.layout.searchOpened); | ||||
|   const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened); | ||||
|   const setMainPanelOpened = useAtomSetter(atoms.layout.mainPanelOpened); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     router.events.on("routeChangeStart", () => { | ||||
|       console.log("[Router Events] on routeChangeStart"); | ||||
|       setSearchOpened(false); | ||||
|       setSettingsOpened(false); | ||||
|       setMainPanelOpened(false); | ||||
|       setSubPanelOpened(false); | ||||
|     }); | ||||
| 
 | ||||
|     router.events.on("hashChangeStart", () => { | ||||
|       console.log("[Router Events] on hashChangeStart"); | ||||
|       setSubPanelOpened(false); | ||||
|     }); | ||||
|   }, [router, setSettingsOpened, setMainPanelOpened, setSubPanelOpened, setSearchOpened]); | ||||
| 
 | ||||
|   useScrollIntoView(); | ||||
| }; | ||||
| @ -1,15 +1,37 @@ | ||||
| import { atom } from "jotai"; | ||||
| import { atomWithStorage } from "jotai/utils"; | ||||
| import { localData } from "contexts/localData"; | ||||
| import { containerQueries } from "contexts/containerQueries"; | ||||
| import { atomPairing } from "helpers/atoms"; | ||||
| import { settings } from "contexts/settings"; | ||||
| import { lightBox } from "contexts/LightBoxProvider"; | ||||
| import { UploadImageFragment } from "graphql/generated"; | ||||
| import { Languages, Currencies, Langui } from "helpers/localData"; | ||||
| 
 | ||||
| /* | ||||
|  * I'm getting a weird error if I put those atoms in appLayout.ts | ||||
|  * So I'm putting the atoms here. Sucks, I know. | ||||
|  */ | ||||
| /* [ LOCAL DATA ATOMS ] */ | ||||
| 
 | ||||
| const languages = atomPairing(atom<Languages>([])); | ||||
| const currencies = atomPairing(atom<Currencies>([])); | ||||
| const langui = atomPairing(atom<Langui>({})); | ||||
| const fallbackLangui = atomPairing(atom<Langui>({})); | ||||
| 
 | ||||
| const localData = { | ||||
|   languages: languages[0], | ||||
|   currencies: currencies[0], | ||||
|   langui: langui[0], | ||||
|   fallbackLangui: fallbackLangui[0], | ||||
| }; | ||||
| 
 | ||||
| /* [ LIGHTBOX ATOMS ] */ | ||||
| 
 | ||||
| const lightBoxAtom = atomPairing( | ||||
|   atom<{ | ||||
|     showLightBox: ( | ||||
|       images: (UploadImageFragment | string | null | undefined)[], | ||||
|       index?: number | ||||
|     ) => void; | ||||
|   }>({ showLightBox: () => null }) | ||||
| ); | ||||
| 
 | ||||
| const lightBox = lightBoxAtom[0]; | ||||
| 
 | ||||
| /* [ APPLAYOUT ATOMS ] */ | ||||
| 
 | ||||
| @ -49,3 +71,9 @@ export const atoms = { | ||||
|   lightBox, | ||||
|   containerQueries, | ||||
| }; | ||||
| 
 | ||||
| // Do not import outside of the "contexts" folder
 | ||||
| export const internalAtoms = { | ||||
|   lightBox: lightBoxAtom, | ||||
|   localData: { languages, currencies, langui, fallbackLangui }, | ||||
| }; | ||||
|  | ||||
| @ -1,42 +1,24 @@ | ||||
| import { atom } from "jotai"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useEffect } from "react"; | ||||
| import { useFetch } from "usehooks-ts"; | ||||
| import { atomPairing, useAtomSetter } from "helpers/atoms"; | ||||
| import { | ||||
|   Languages, | ||||
|   Currencies, | ||||
|   Langui, | ||||
|   processLangui, | ||||
|   processCurrencies, | ||||
|   processLanguages, | ||||
| } from "helpers/localData"; | ||||
| import { useAtomSetter } from "helpers/atoms"; | ||||
| 
 | ||||
| import { | ||||
|   LocalDataGetWebsiteInterfacesQuery, | ||||
|   LocalDataGetCurrenciesQuery, | ||||
|   LocalDataGetLanguagesQuery, | ||||
| } from "graphql/generated"; | ||||
| import { LocalDataFile } from "graphql/fetchLocalData"; | ||||
| 
 | ||||
| const languages = atomPairing(atom<Languages>([])); | ||||
| const currencies = atomPairing(atom<Currencies>([])); | ||||
| const langui = atomPairing(atom<Langui>({})); | ||||
| const fallbackLangui = atomPairing(atom<Langui>({})); | ||||
| 
 | ||||
| export const localData = { | ||||
|   languages: languages[0], | ||||
|   currencies: currencies[0], | ||||
|   langui: langui[0], | ||||
|   fallbackLangui: fallbackLangui[0], | ||||
| }; | ||||
| import { internalAtoms } from "contexts/atoms"; | ||||
| import { processLanguages, processCurrencies, processLangui } from "helpers/localData"; | ||||
| 
 | ||||
| const getFileName = (name: LocalDataFile): string => `/local-data/${name}.json`; | ||||
| 
 | ||||
| export const useLocalData = (): void => { | ||||
|   const setLanguages = useAtomSetter(languages); | ||||
|   const setCurrencies = useAtomSetter(currencies); | ||||
|   const setLangui = useAtomSetter(langui); | ||||
|   const setFallbackLangui = useAtomSetter(fallbackLangui); | ||||
|   const setLanguages = useAtomSetter(internalAtoms.localData.languages); | ||||
|   const setCurrencies = useAtomSetter(internalAtoms.localData.currencies); | ||||
|   const setLangui = useAtomSetter(internalAtoms.localData.langui); | ||||
|   const setFallbackLangui = useAtomSetter(internalAtoms.localData.fallbackLangui); | ||||
| 
 | ||||
|   const { locale } = useRouter(); | ||||
|   const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages")); | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| import { isDefined } from "./asserts"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { insertInBetweenArray } from "helpers/others"; | ||||
| import { isDefined } from "helpers/asserts"; | ||||
| 
 | ||||
| export interface Wrapper { | ||||
|   children: React.ReactNode; | ||||
| @ -28,3 +30,15 @@ export const ConditionalWrapper = <T, U>({ | ||||
|   ) : ( | ||||
|     <>{children}</> | ||||
|   ); | ||||
| 
 | ||||
| interface ElementsSeparatorProps { | ||||
|   children: React.ReactNode[]; | ||||
|   separator?: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| export const ElementsSeparator = ({ | ||||
|   children, | ||||
|   separator = <HorizontalLine />, | ||||
| }: ElementsSeparatorProps): JSX.Element => ( | ||||
|   <>{insertInBetweenArray(children.filter(Boolean), separator)}</> | ||||
| ); | ||||
|  | ||||
| @ -46,3 +46,17 @@ export const cartesianProduct = <T, U>(arrayA: T[], arrayB: U[]): [T, U][] => { | ||||
|   arrayA.forEach((a) => arrayB.forEach((b) => result.push([a, b]))); | ||||
|   return result; | ||||
| }; | ||||
| 
 | ||||
| export const insertInBetweenArray = <T>(elems: T[], elemToInsert: T): T[] => { | ||||
|   if (elems.length < 2) return elems; | ||||
|   const elemsCopy = [...elems]; | ||||
|   const lastElem = elemsCopy.pop() as T; | ||||
| 
 | ||||
|   const result: T[] = []; | ||||
|   for (const elem of elemsCopy) { | ||||
|     result.push(elem, elemToInsert); | ||||
|   } | ||||
|   result.push(lastElem); | ||||
| 
 | ||||
|   return result; | ||||
| }; | ||||
|  | ||||
| @ -16,20 +16,20 @@ import "styles/rc-slider.css"; | ||||
| import "styles/tippy.css"; | ||||
| 
 | ||||
| import { useLocalData } from "contexts/localData"; | ||||
| import { useAppLayout } from "contexts/appLayout"; | ||||
| import { LightBoxProvider } from "contexts/LightBoxProvider"; | ||||
| import { SettingsPopup } from "components/Panels/SettingsPopup"; | ||||
| import { useSettings } from "contexts/settings"; | ||||
| import { useContainerQueries } from "contexts/containerQueries"; | ||||
| import { useWebkitFixes } from "contexts/webkitFixes"; | ||||
| import { SearchPopup } from "components/Panels/SearchPopup"; | ||||
| import { useScrollIntoView } from "hooks/useScrollIntoView"; | ||||
| 
 | ||||
| const AccordsLibraryApp = (props: AppProps): JSX.Element => { | ||||
|   useLocalData(); | ||||
|   useAppLayout(); | ||||
|   useSettings(); | ||||
|   useContainerQueries(); | ||||
|   useWebkitFixes(); | ||||
|   useScrollIntoView(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useCallback } from "react"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Ico } from "components/Ico"; | ||||
| @ -16,7 +17,7 @@ import { filterHasAttributes, isDefined } from "helpers/asserts"; | ||||
| import { getVideoFile } from "helpers/videos"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomGetter } from "helpers/atoms"; | ||||
| import { useAtomGetter, useAtomSetter } from "helpers/atoms"; | ||||
| import { Link } from "components/Inputs/Link"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| @ -32,6 +33,8 @@ interface Props extends AppLayoutRequired { | ||||
| 
 | ||||
| const Video = ({ video, ...otherProps }: Props): JSX.Element => { | ||||
|   const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
|   const closeSubPanel = useCallback(() => setSubPanelOpened(false), [setSubPanelOpened]); | ||||
|   const { format } = useFormat(); | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
| @ -45,9 +48,9 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => { | ||||
| 
 | ||||
|       <HorizontalLine /> | ||||
| 
 | ||||
|       <NavOption title={format("video")} url="#video" border /> | ||||
|       <NavOption title={format("channel")} url="#channel" border /> | ||||
|       <NavOption title={format("description")} url="#description" border /> | ||||
|       <NavOption title={format("video")} url="#video" border onClick={closeSubPanel} /> | ||||
|       <NavOption title={format("channel")} url="#channel" border onClick={closeSubPanel} /> | ||||
|       <NavOption title={format("description")} url="#description" border onClick={closeSubPanel} /> | ||||
|     </SubPanel> | ||||
|   ); | ||||
| 
 | ||||
| @ -69,7 +72,6 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => { | ||||
|               src={`https://www.youtube-nocookie.com/embed/${video.uid}`} | ||||
|               className="aspect-video w-full" | ||||
|               title="YouTube video player" | ||||
|               frameBorder="0" | ||||
|               allow="accelerometer; autoplay; clipboard-write; | ||||
|               encrypted-media; gyroscope; picture-in-picture" | ||||
|               allowFullScreen | ||||
|  | ||||
| @ -21,6 +21,7 @@ import { useScrollTopOnChange } from "hooks/useScrollTopOnChange"; | ||||
| import { Ids } from "types/ids"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| import { ElementsSeparator } from "helpers/component"; | ||||
| 
 | ||||
| /* | ||||
|  *                                           ╭────────╮ | ||||
| @ -87,29 +88,29 @@ const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element = | ||||
|       ) : ( | ||||
|         <> | ||||
|           {selectedContentTranslation && ( | ||||
|             <> | ||||
|               <ThumbnailHeader | ||||
|                 pre_title={selectedContentTranslation.pre_title} | ||||
|                 title={selectedContentTranslation.title} | ||||
|                 subtitle={selectedContentTranslation.subtitle} | ||||
|                 languageSwitcher={ | ||||
|                   ContentLanguageSwitcherProps.locales.size > 1 ? ( | ||||
|                     <ContentLanguageSwitcher {...ContentLanguageSwitcherProps} /> | ||||
|                   ) : undefined | ||||
|                 } | ||||
|                 categories={primaryContent?.categories} | ||||
|                 type={primaryContent?.type} | ||||
|                 description={selectedContentTranslation.description} | ||||
|                 thumbnail={primaryContent?.thumbnail?.data?.attributes} | ||||
|               /> | ||||
|             <ElementsSeparator> | ||||
|               {[ | ||||
|                 <ThumbnailHeader | ||||
|                   key="thumbnailHeader" | ||||
|                   pre_title={selectedContentTranslation.pre_title} | ||||
|                   title={selectedContentTranslation.title} | ||||
|                   subtitle={selectedContentTranslation.subtitle} | ||||
|                   languageSwitcher={ | ||||
|                     ContentLanguageSwitcherProps.locales.size > 1 ? ( | ||||
|                       <ContentLanguageSwitcher {...ContentLanguageSwitcherProps} /> | ||||
|                     ) : undefined | ||||
|                   } | ||||
|                   categories={primaryContent?.categories} | ||||
|                   type={primaryContent?.type} | ||||
|                   description={selectedContentTranslation.description} | ||||
|                   thumbnail={primaryContent?.thumbnail?.data?.attributes} | ||||
|                 />, | ||||
| 
 | ||||
|               {selectedContentTranslation.text_set?.text && ( | ||||
|                 <> | ||||
|                   <HorizontalLine /> | ||||
|                 selectedContentTranslation.text_set?.text && ( | ||||
|                   <Markdawn text={selectedContentTranslation.text_set.text} /> | ||||
|                 </> | ||||
|               )} | ||||
|             </> | ||||
|                 ), | ||||
|               ]} | ||||
|             </ElementsSeparator> | ||||
|           )} | ||||
|         </> | ||||
|       )} | ||||
|  | ||||
| @ -3,7 +3,6 @@ import { Fragment, useCallback } from "react"; | ||||
| import naturalCompare from "string-natural-compare"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs"; | ||||
| import { Markdawn, TableOfContents } from "components/Markdown/Markdawn"; | ||||
| import { TranslatedReturnButton } from "components/PanelComponents/ReturnButton"; | ||||
| @ -32,9 +31,10 @@ import { TranslatedPreviewLine } from "components/PreviewLine"; | ||||
| import { cIf } from "helpers/className"; | ||||
| import { Ids } from "types/ids"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomGetter } from "helpers/atoms"; | ||||
| import { useAtomGetter, useAtomSetter } from "helpers/atoms"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| import { ElementsSeparator } from "helpers/component"; | ||||
| 
 | ||||
| /* | ||||
|  *                                           ╭────────╮ | ||||
| @ -47,6 +47,7 @@ interface Props extends AppLayoutRequired { | ||||
| 
 | ||||
| const Content = ({ content, ...otherProps }: Props): JSX.Element => { | ||||
|   const isContentPanelAtLeast2xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast2xl); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
|   const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout); | ||||
| 
 | ||||
|   const { format, formatStatusDescription } = useFormat(); | ||||
| @ -92,167 +93,165 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => { | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
|       <TranslatedReturnButton {...returnButtonProps} displayOnlyOn="3ColumnsLayout" /> | ||||
|       <ElementsSeparator> | ||||
|         {[ | ||||
|           !is1ColumnLayout && <TranslatedReturnButton {...returnButtonProps} />, | ||||
| 
 | ||||
|       {selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && ( | ||||
|         <> | ||||
|           <HorizontalLine /> | ||||
|           <div className="grid gap-5"> | ||||
|             <h2 className="text-xl"> | ||||
|               {selectedTranslation.text_set.source_language.data.attributes.code === | ||||
|               selectedTranslation.language?.data?.attributes?.code | ||||
|                 ? format("transcript_notice") | ||||
|                 : format("translation_notice")} | ||||
|             </h2> | ||||
|           selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && ( | ||||
|             <div className="grid gap-5"> | ||||
|               <h2 className="text-xl"> | ||||
|                 {selectedTranslation.text_set.source_language.data.attributes.code === | ||||
|                 selectedTranslation.language?.data?.attributes?.code | ||||
|                   ? format("transcript_notice") | ||||
|                   : format("translation_notice")} | ||||
|               </h2> | ||||
| 
 | ||||
|             {selectedTranslation.text_set.source_language.data.attributes.code !== | ||||
|               selectedTranslation.language?.data?.attributes?.code && ( | ||||
|               <div className="grid place-items-center gap-2"> | ||||
|                 <p className="font-headers font-bold">{format("source_language")}:</p> | ||||
|                 <Chip | ||||
|                   text={prettyLanguage( | ||||
|                     selectedTranslation.text_set.source_language.data.attributes.code, | ||||
|                     languages | ||||
|                   )} | ||||
|                 /> | ||||
|               {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 font-bold">{format("source_language")}:</p> | ||||
|                   <Chip | ||||
|                     text={prettyLanguage( | ||||
|                       selectedTranslation.text_set.source_language.data.attributes.code, | ||||
|                       languages | ||||
|                     )} | ||||
|                   /> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|               <div className="grid grid-flow-col place-content-center place-items-center gap-2"> | ||||
|                 <p className="font-headers font-bold">{format("status")}:</p> | ||||
| 
 | ||||
|                 <ToolTip | ||||
|                   content={formatStatusDescription(selectedTranslation.text_set.status)} | ||||
|                   maxWidth={"20rem"}> | ||||
|                   <Chip text={selectedTranslation.text_set.status} /> | ||||
|                 </ToolTip> | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             <div className="grid grid-flow-col place-content-center place-items-center gap-2"> | ||||
|               <p className="font-headers font-bold">{format("status")}:</p> | ||||
| 
 | ||||
|               <ToolTip | ||||
|                 content={formatStatusDescription(selectedTranslation.text_set.status)} | ||||
|                 maxWidth={"20rem"}> | ||||
|                 <Chip text={selectedTranslation.text_set.status} /> | ||||
|               </ToolTip> | ||||
|             </div> | ||||
| 
 | ||||
|             {selectedTranslation.text_set.transcribers && | ||||
|               selectedTranslation.text_set.transcribers.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold">{format("transcribers")}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(selectedTranslation.text_set.transcribers.data, [ | ||||
|                       "attributes", | ||||
|                       "id", | ||||
|                     ] as const).map((recorder) => ( | ||||
|                       <Fragment key={recorder.id}> | ||||
|                         <RecorderChip recorder={recorder.attributes} /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|             {selectedTranslation.text_set.translators && | ||||
|               selectedTranslation.text_set.translators.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold">{format("translators")}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(selectedTranslation.text_set.translators.data, [ | ||||
|                       "attributes", | ||||
|                       "id", | ||||
|                     ] as const).map((recorder) => ( | ||||
|                       <Fragment key={recorder.id}> | ||||
|                         <RecorderChip recorder={recorder.attributes} /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|             {selectedTranslation.text_set.proofreaders && | ||||
|               selectedTranslation.text_set.proofreaders.data.length > 0 && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold">{format("proofreaders")}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     {filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [ | ||||
|                       "attributes", | ||||
|                       "id", | ||||
|                     ] as const).map((recorder) => ( | ||||
|                       <Fragment key={recorder.id}> | ||||
|                         <RecorderChip recorder={recorder.attributes} /> | ||||
|                       </Fragment> | ||||
|                     ))} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|             {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && ( | ||||
|               <div> | ||||
|                 <p className="font-headers font-bold">{format("notes")}:</p> | ||||
|                 <div className="grid place-content-center place-items-center gap-2"> | ||||
|                   <Markdawn text={selectedTranslation.text_set.notes} /> | ||||
|                 </div> | ||||
|               </div> | ||||
|             )} | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
| 
 | ||||
|       {selectedTranslation?.text_set?.text && ( | ||||
|         <> | ||||
|           <TableOfContents | ||||
|             text={selectedTranslation.text_set.text} | ||||
|             title={prettyInlineTitle( | ||||
|               selectedTranslation.pre_title, | ||||
|               selectedTranslation.title, | ||||
|               selectedTranslation.subtitle | ||||
|             )} | ||||
|             horizontalLine | ||||
|           /> | ||||
|         </> | ||||
|       )} | ||||
| 
 | ||||
|       {content.ranged_contents?.data && content.ranged_contents.data.length > 0 && ( | ||||
|         <> | ||||
|           <HorizontalLine /> | ||||
|           <div> | ||||
|             <p className="font-headers text-2xl font-bold">{format("source")}</p> | ||||
|             <div className="mt-6 grid place-items-center gap-6"> | ||||
|               {filterHasAttributes(content.ranged_contents.data, [ | ||||
|                 "attributes.library_item.data.attributes", | ||||
|                 "attributes.library_item.data.id", | ||||
|               ] as const).map((rangedContent) => { | ||||
|                 const libraryItem = rangedContent.attributes.library_item.data; | ||||
|                 return ( | ||||
|                   <div key={libraryItem.attributes.slug} className={cIf(is1ColumnLayout, "w-3/4")}> | ||||
|                     <PreviewCard | ||||
|                       href={`/library/${libraryItem.attributes.slug}`} | ||||
|                       title={libraryItem.attributes.title} | ||||
|                       subtitle={libraryItem.attributes.subtitle} | ||||
|                       thumbnail={libraryItem.attributes.thumbnail?.data?.attributes} | ||||
|                       thumbnailAspectRatio="21/29.7" | ||||
|                       thumbnailRounded={false} | ||||
|                       topChips={ | ||||
|                         libraryItem.attributes.metadata && | ||||
|                         libraryItem.attributes.metadata.length > 0 && | ||||
|                         libraryItem.attributes.metadata[0] | ||||
|                           ? [prettyItemSubType(libraryItem.attributes.metadata[0])] | ||||
|                           : [] | ||||
|                       } | ||||
|                       bottomChips={filterHasAttributes(libraryItem.attributes.categories?.data, [ | ||||
|               {selectedTranslation.text_set.transcribers && | ||||
|                 selectedTranslation.text_set.transcribers.data.length > 0 && ( | ||||
|                   <div> | ||||
|                     <p className="font-headers font-bold">{format("transcribers")}:</p> | ||||
|                     <div className="grid place-content-center place-items-center gap-2"> | ||||
|                       {filterHasAttributes(selectedTranslation.text_set.transcribers.data, [ | ||||
|                         "attributes", | ||||
|                       ] as const).map((category) => category.attributes.short)} | ||||
|                       metadata={{ | ||||
|                         releaseDate: libraryItem.attributes.release_date, | ||||
|                         price: libraryItem.attributes.price, | ||||
|                         position: "Bottom", | ||||
|                       }} | ||||
|                       infoAppend={ | ||||
|                         !isUntangibleGroupItem(libraryItem.attributes.metadata?.[0]) && ( | ||||
|                           <PreviewCardCTAs id={libraryItem.id} /> | ||||
|                         ) | ||||
|                       } | ||||
|                     /> | ||||
|                         "id", | ||||
|                       ] as const).map((recorder) => ( | ||||
|                         <Fragment key={recorder.id}> | ||||
|                           <RecorderChip recorder={recorder.attributes} /> | ||||
|                         </Fragment> | ||||
|                       ))} | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 ); | ||||
|               })} | ||||
|                 )} | ||||
| 
 | ||||
|               {selectedTranslation.text_set.translators && | ||||
|                 selectedTranslation.text_set.translators.data.length > 0 && ( | ||||
|                   <div> | ||||
|                     <p className="font-headers font-bold">{format("translators")}:</p> | ||||
|                     <div className="grid place-content-center place-items-center gap-2"> | ||||
|                       {filterHasAttributes(selectedTranslation.text_set.translators.data, [ | ||||
|                         "attributes", | ||||
|                         "id", | ||||
|                       ] as const).map((recorder) => ( | ||||
|                         <Fragment key={recorder.id}> | ||||
|                           <RecorderChip recorder={recorder.attributes} /> | ||||
|                         </Fragment> | ||||
|                       ))} | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 )} | ||||
| 
 | ||||
|               {selectedTranslation.text_set.proofreaders && | ||||
|                 selectedTranslation.text_set.proofreaders.data.length > 0 && ( | ||||
|                   <div> | ||||
|                     <p className="font-headers font-bold">{format("proofreaders")}:</p> | ||||
|                     <div className="grid place-content-center place-items-center gap-2"> | ||||
|                       {filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [ | ||||
|                         "attributes", | ||||
|                         "id", | ||||
|                       ] as const).map((recorder) => ( | ||||
|                         <Fragment key={recorder.id}> | ||||
|                           <RecorderChip recorder={recorder.attributes} /> | ||||
|                         </Fragment> | ||||
|                       ))} | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 )} | ||||
| 
 | ||||
|               {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && ( | ||||
|                 <div> | ||||
|                   <p className="font-headers font-bold">{format("notes")}:</p> | ||||
|                   <div className="grid place-content-center place-items-center gap-2"> | ||||
|                     <Markdawn text={selectedTranslation.text_set.notes} /> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
|           ), | ||||
| 
 | ||||
|           selectedTranslation?.text_set?.text && ( | ||||
|             <TableOfContents | ||||
|               text={selectedTranslation.text_set.text} | ||||
|               title={prettyInlineTitle( | ||||
|                 selectedTranslation.pre_title, | ||||
|                 selectedTranslation.title, | ||||
|                 selectedTranslation.subtitle | ||||
|               )} | ||||
|               onContentClicked={() => setSubPanelOpened(false)} | ||||
|             /> | ||||
|           ), | ||||
| 
 | ||||
|           content.ranged_contents?.data && content.ranged_contents.data.length > 0 && ( | ||||
|             <div> | ||||
|               <p className="font-headers text-2xl font-bold">{format("source")}</p> | ||||
|               <div className="mt-6 grid place-items-center gap-6"> | ||||
|                 {filterHasAttributes(content.ranged_contents.data, [ | ||||
|                   "attributes.library_item.data.attributes", | ||||
|                   "attributes.library_item.data.id", | ||||
|                 ] as const).map((rangedContent) => { | ||||
|                   const libraryItem = rangedContent.attributes.library_item.data; | ||||
|                   return ( | ||||
|                     <div | ||||
|                       key={libraryItem.attributes.slug} | ||||
|                       className={cIf(is1ColumnLayout, "w-3/4")}> | ||||
|                       <PreviewCard | ||||
|                         href={`/library/${libraryItem.attributes.slug}`} | ||||
|                         title={libraryItem.attributes.title} | ||||
|                         subtitle={libraryItem.attributes.subtitle} | ||||
|                         thumbnail={libraryItem.attributes.thumbnail?.data?.attributes} | ||||
|                         thumbnailAspectRatio="21/29.7" | ||||
|                         thumbnailRounded={false} | ||||
|                         topChips={ | ||||
|                           libraryItem.attributes.metadata && | ||||
|                           libraryItem.attributes.metadata.length > 0 && | ||||
|                           libraryItem.attributes.metadata[0] | ||||
|                             ? [prettyItemSubType(libraryItem.attributes.metadata[0])] | ||||
|                             : [] | ||||
|                         } | ||||
|                         bottomChips={filterHasAttributes(libraryItem.attributes.categories?.data, [ | ||||
|                           "attributes", | ||||
|                         ] as const).map((category) => category.attributes.short)} | ||||
|                         metadata={{ | ||||
|                           releaseDate: libraryItem.attributes.release_date, | ||||
|                           price: libraryItem.attributes.price, | ||||
|                           position: "Bottom", | ||||
|                         }} | ||||
|                         infoAppend={ | ||||
|                           !isUntangibleGroupItem(libraryItem.attributes.metadata?.[0]) && ( | ||||
|                             <PreviewCardCTAs id={libraryItem.id} /> | ||||
|                           ) | ||||
|                         } | ||||
|                       /> | ||||
|                     </div> | ||||
|                   ); | ||||
|                 })} | ||||
|               </div> | ||||
|             </div> | ||||
|           ), | ||||
|         ]} | ||||
|       </ElementsSeparator> | ||||
|     </SubPanel> | ||||
|   ); | ||||
| 
 | ||||
| @ -265,100 +264,102 @@ const Content = ({ content, ...otherProps }: Props): JSX.Element => { | ||||
|       /> | ||||
| 
 | ||||
|       <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} | ||||
|           languageSwitcher={ | ||||
|             languageSwitcherProps.locales.size > 1 ? ( | ||||
|               <LanguageSwitcher {...languageSwitcherProps} /> | ||||
|             ) : undefined | ||||
|           } | ||||
|         /> | ||||
|         <ElementsSeparator> | ||||
|           {[ | ||||
|             <ThumbnailHeader | ||||
|               key="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} | ||||
|               languageSwitcher={ | ||||
|                 languageSwitcherProps.locales.size > 1 ? ( | ||||
|                   <LanguageSwitcher {...languageSwitcherProps} /> | ||||
|                 ) : undefined | ||||
|               } | ||||
|             />, | ||||
|             <> | ||||
|               {previousContent?.attributes && ( | ||||
|                 <div className="mb-6 w-full"> | ||||
|                   <h2 className="mb-4 text-center text-2xl">{format("previous_content")}</h2> | ||||
|                   <TranslatedPreviewLine | ||||
|                     href={`/contents/${previousContent.attributes.slug}`} | ||||
|                     translations={filterHasAttributes(previousContent.attributes.translations, [ | ||||
|                       "language.data.attributes.code", | ||||
|                     ] as const).map((translation) => ({ | ||||
|                       pre_title: translation.pre_title, | ||||
|                       title: translation.title, | ||||
|                       subtitle: translation.subtitle, | ||||
|                       language: translation.language.data.attributes.code, | ||||
|                     }))} | ||||
|                     fallback={{ | ||||
|                       title: prettySlug(previousContent.attributes.slug), | ||||
|                     }} | ||||
|                     thumbnail={previousContent.attributes.thumbnail?.data?.attributes} | ||||
|                     topChips={ | ||||
|                       isContentPanelAtLeast2xl && 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={ | ||||
|                       isContentPanelAtLeast2xl | ||||
|                         ? previousContent.attributes.categories?.data.map( | ||||
|                             (category) => category.attributes?.short ?? "" | ||||
|                           ) | ||||
|                         : undefined | ||||
|                     } | ||||
|                   /> | ||||
|                 </div> | ||||
|               )} | ||||
|             </>, | ||||
| 
 | ||||
|         {previousContent?.attributes && ( | ||||
|           <div className="mt-12 mb-8 w-full"> | ||||
|             <h2 className="mb-4 text-center text-2xl">{format("previous_content")}</h2> | ||||
|             <TranslatedPreviewLine | ||||
|               href={`/contents/${previousContent.attributes.slug}`} | ||||
|               translations={filterHasAttributes(previousContent.attributes.translations, [ | ||||
|                 "language.data.attributes.code", | ||||
|               ] as const).map((translation) => ({ | ||||
|                 pre_title: translation.pre_title, | ||||
|                 title: translation.title, | ||||
|                 subtitle: translation.subtitle, | ||||
|                 language: translation.language.data.attributes.code, | ||||
|               }))} | ||||
|               fallback={{ | ||||
|                 title: prettySlug(previousContent.attributes.slug), | ||||
|               }} | ||||
|               thumbnail={previousContent.attributes.thumbnail?.data?.attributes} | ||||
|               topChips={ | ||||
|                 isContentPanelAtLeast2xl && 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={ | ||||
|                 isContentPanelAtLeast2xl | ||||
|                   ? previousContent.attributes.categories?.data.map( | ||||
|                       (category) => category.attributes?.short ?? "" | ||||
|                     ) | ||||
|                   : undefined | ||||
|               } | ||||
|             /> | ||||
|           </div> | ||||
|         )} | ||||
|             selectedTranslation?.text_set?.text && ( | ||||
|               <Markdawn text={selectedTranslation.text_set.text} /> | ||||
|             ), | ||||
| 
 | ||||
|         {selectedTranslation?.text_set?.text && ( | ||||
|           <> | ||||
|             <HorizontalLine /> | ||||
|             <Markdawn text={selectedTranslation.text_set.text} /> | ||||
|           </> | ||||
|         )} | ||||
| 
 | ||||
|         {nextContent?.attributes && ( | ||||
|           <> | ||||
|             <HorizontalLine /> | ||||
|             <h2 className="mb-4 text-center text-2xl">{format("followup_content")}</h2> | ||||
|             <TranslatedPreviewLine | ||||
|               href={`/contents/${nextContent.attributes.slug}`} | ||||
|               translations={filterHasAttributes(nextContent.attributes.translations, [ | ||||
|                 "language.data.attributes.code", | ||||
|               ] as const).map((translation) => ({ | ||||
|                 pre_title: translation.pre_title, | ||||
|                 title: translation.title, | ||||
|                 subtitle: translation.subtitle, | ||||
|                 language: translation.language.data.attributes.code, | ||||
|               }))} | ||||
|               fallback={{ title: nextContent.attributes.slug }} | ||||
|               thumbnail={nextContent.attributes.thumbnail?.data?.attributes} | ||||
|               topChips={ | ||||
|                 isContentPanelAtLeast2xl && 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={ | ||||
|                 isContentPanelAtLeast2xl | ||||
|                   ? nextContent.attributes.categories?.data.map( | ||||
|                       (category) => category.attributes?.short ?? "" | ||||
|                     ) | ||||
|                   : undefined | ||||
|               } | ||||
|             /> | ||||
|           </> | ||||
|         )} | ||||
|             nextContent?.attributes && ( | ||||
|               <> | ||||
|                 <h2 className="mb-4 text-center text-2xl">{format("followup_content")}</h2> | ||||
|                 <TranslatedPreviewLine | ||||
|                   href={`/contents/${nextContent.attributes.slug}`} | ||||
|                   translations={filterHasAttributes(nextContent.attributes.translations, [ | ||||
|                     "language.data.attributes.code", | ||||
|                   ] as const).map((translation) => ({ | ||||
|                     pre_title: translation.pre_title, | ||||
|                     title: translation.title, | ||||
|                     subtitle: translation.subtitle, | ||||
|                     language: translation.language.data.attributes.code, | ||||
|                   }))} | ||||
|                   fallback={{ title: nextContent.attributes.slug }} | ||||
|                   thumbnail={nextContent.attributes.thumbnail?.data?.attributes} | ||||
|                   topChips={ | ||||
|                     isContentPanelAtLeast2xl && 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={ | ||||
|                     isContentPanelAtLeast2xl | ||||
|                       ? nextContent.attributes.categories?.data.map( | ||||
|                           (category) => category.attributes?.short ?? "" | ||||
|                         ) | ||||
|                       : undefined | ||||
|                   } | ||||
|                 /> | ||||
|               </> | ||||
|             ), | ||||
|           ]} | ||||
|         </ElementsSeparator> | ||||
|       </div> | ||||
|     </ContentPanel> | ||||
|   ); | ||||
|  | ||||
| @ -29,6 +29,8 @@ import { prettySlug } from "helpers/formatters"; | ||||
| import { Paginator } from "components/Containers/Paginator"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| import { useAtomSetter } from "helpers/atoms"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| 
 | ||||
| /* | ||||
|  *                                         ╭─────────────╮ | ||||
| @ -59,6 +61,7 @@ const Contents = (props: Props): JSX.Element => { | ||||
|   const hoverable = useDeviceSupportsHover(); | ||||
|   const { format } = useFormat(); | ||||
|   const router = useTypedRouter(queryParamSchema); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
| 
 | ||||
|   const sortingMethods = useMemo( | ||||
|     () => [ | ||||
| @ -136,7 +139,12 @@ const Contents = (props: Props): JSX.Element => { | ||||
| 
 | ||||
|       <HorizontalLine /> | ||||
| 
 | ||||
|       <Button href="/contents" text={format("switch_to_folder_view")} icon="folder" /> | ||||
|       <Button | ||||
|         href="/contents" | ||||
|         text={format("switch_to_folder_view")} | ||||
|         icon="folder" | ||||
|         onClick={() => setSubPanelOpened(false)} | ||||
|       /> | ||||
| 
 | ||||
|       <HorizontalLine /> | ||||
| 
 | ||||
|  | ||||
| @ -17,7 +17,7 @@ import { TranslatedPreviewCard } from "components/PreviewCard"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { cJoin, cIf } from "helpers/className"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomGetter } from "helpers/atoms"; | ||||
| import { useAtomGetter, useAtomSetter } from "helpers/atoms"; | ||||
| import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| @ -35,6 +35,7 @@ interface Props extends AppLayoutRequired { | ||||
| 
 | ||||
| const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Element => { | ||||
|   const { format } = useFormat(); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
|   const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
| @ -47,7 +48,12 @@ const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Elemen | ||||
| 
 | ||||
|       <HorizontalLine /> | ||||
| 
 | ||||
|       <Button href="/contents/all" text={format("switch_to_grid_view")} icon="apps" /> | ||||
|       <Button | ||||
|         href="/contents/all" | ||||
|         text={format("switch_to_grid_view")} | ||||
|         icon="apps" | ||||
|         onClick={() => setSubPanelOpened(false)} | ||||
|       /> | ||||
|     </SubPanel> | ||||
|   ); | ||||
| 
 | ||||
|  | ||||
| @ -41,8 +41,8 @@ const Home = (props: PostStaticProps): JSX.Element => { | ||||
|       prependBody={ | ||||
|         <div className="grid w-full place-content-center place-items-center gap-5 text-center"> | ||||
|           <div | ||||
|             className="aspect-square w-32 bg-black [mask:url('/icons/accords.svg')] | ||||
|             [mask-size:contain] [mask-repeat:no-repeat] [mask-position:center]" | ||||
|             className="aspect-square w-32 bg-black ![mask-size:contain] | ||||
|             [mask:url('/icons/accords.svg')]" | ||||
|           /> | ||||
|           <h1 className="mb-0 text-5xl">Accord’s Library</h1> | ||||
|           <h2 className="-mt-5 text-xl">Discover • Analyze • Translate • Archive</h2> | ||||
|  | ||||
| @ -50,7 +50,7 @@ import { useIntersectionList } from "hooks/useIntersectionList"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| import { Ids } from "types/ids"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomGetter } from "helpers/atoms"; | ||||
| import { useAtomGetter, useAtomSetter } from "helpers/atoms"; | ||||
| import { Link } from "components/Inputs/Link"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| @ -85,6 +85,8 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { | ||||
|   const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(false); | ||||
| 
 | ||||
|   const { showLightBox } = useAtomGetter(atoms.lightBox); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
|   const closeSubPanel = useCallback(() => setSubPanelOpened(false), [setSubPanelOpened]); | ||||
| 
 | ||||
|   useScrollTopOnChange(Ids.ContentPanel, [item]); | ||||
|   const currentIntersection = useIntersectionList(intersectionIds); | ||||
| @ -109,6 +111,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { | ||||
|           url={`#${intersectionIds[0]}`} | ||||
|           border | ||||
|           active={currentIntersection === 0} | ||||
|           onClick={closeSubPanel} | ||||
|         /> | ||||
| 
 | ||||
|         {item.gallery && item.gallery.data.length > 0 && ( | ||||
| @ -117,6 +120,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { | ||||
|             url={`#${intersectionIds[1]}`} | ||||
|             border | ||||
|             active={currentIntersection === 1} | ||||
|             onClick={closeSubPanel} | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
| @ -125,6 +129,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { | ||||
|           url={`#${intersectionIds[2]}`} | ||||
|           border | ||||
|           active={currentIntersection === 2} | ||||
|           onClick={closeSubPanel} | ||||
|         /> | ||||
| 
 | ||||
|         {item.subitems && item.subitems.data.length > 0 && ( | ||||
| @ -133,6 +138,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { | ||||
|             url={`#${intersectionIds[3]}`} | ||||
|             border | ||||
|             active={currentIntersection === 3} | ||||
|             onClick={closeSubPanel} | ||||
|           /> | ||||
|         )} | ||||
| 
 | ||||
| @ -142,6 +148,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { | ||||
|             url={`#${intersectionIds[4]}`} | ||||
|             border | ||||
|             active={currentIntersection === 4} | ||||
|             onClick={closeSubPanel} | ||||
|           /> | ||||
|         )} | ||||
|       </div> | ||||
|  | ||||
| @ -931,7 +931,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps): | ||||
|                last-of-type:border-0`,
 | ||||
|               cIf( | ||||
|                 is1ColumnLayout, | ||||
|                 "grid-cols-2 gap-[4vmin]", | ||||
|                 "grid-cols-3 gap-[4vmin]", | ||||
|                 "grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))]" | ||||
|               ) | ||||
|             )}> | ||||
|  | ||||
| @ -50,11 +50,11 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => { | ||||
|   }); | ||||
|   const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|   const subPanel = is3ColumnsLayout ? ( | ||||
|     <SubPanel> | ||||
|       <ReturnButton href={`/wiki`} title={format("wiki")} displayOnlyOn={"3ColumnsLayout"} /> | ||||
|     </SubPanel> | ||||
|   ); | ||||
|   ) : undefined; | ||||
| 
 | ||||
|   const contentPanel = ( | ||||
|     <ContentPanel width={ContentPanelWidthSizes.Large}> | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
|   max-width: calc(100vw - 10px); | ||||
| } | ||||
| .tippy-box { | ||||
|   @apply relative rounded-lg bg-light shadow-xl | ||||
|   @apply relative rounded-lg bg-highlight shadow-xl | ||||
|   transition-[transform,visibility,opacity] shadow-shade; | ||||
| } | ||||
| .tippy-box[data-placement^="top"] > .tippy-arrow { | ||||
| @ -50,7 +50,7 @@ | ||||
|   transition-timing-function: cubic-bezier(0.54, 1.5, 0.38, 1.11); | ||||
| } | ||||
| .tippy-arrow { | ||||
|   @apply h-4 w-4 text-light; | ||||
|   @apply h-4 w-4 text-highlight; | ||||
| } | ||||
| .tippy-arrow:before { | ||||
|   content: ""; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint