Merge pull request #23 from Accords-Library/react18

React18
This commit is contained in:
DrMint 2022-05-15 10:21:37 +02:00 committed by GitHub
commit 5de174d63e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 3123 additions and 3127 deletions

View File

@ -50,6 +50,7 @@ module.exports = {
"max-classes-per-file": ["error", 1], "max-classes-per-file": ["error", 1],
// "max-depth": ["warn", 4], // "max-depth": ["warn", 4],
// "max-lines": "warn", // "max-lines": "warn",
"max-len": ["warn", { code: 100 }],
// "max-lines-per-function": "warn", // "max-lines-per-function": "warn",
// "max-nested-callbacks": "warn", // "max-nested-callbacks": "warn",
// "max-params": "warn", // "max-params": "warn",

View File

@ -25,6 +25,7 @@
#### [Front](https://github.com/Accords-Library/accords-library.com) (this repository) #### [Front](https://github.com/Accords-Library/accords-library.com) (this repository)
- Language: [TypeScript](https://www.typescriptlang.org/) - Language: [TypeScript](https://www.typescriptlang.org/)
- Framework: [Next.js](https://nextjs.org/) (React)
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/) - Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
- Fetch the GraphQL schema from the GraphQL back-end endpoint - Fetch the GraphQL schema from the GraphQL back-end endpoint
- Read the operations and fragments stored as graphql files in the `src/graphql` folder - Read the operations and fragments stored as graphql files in the `src/graphql` folder
@ -33,28 +34,34 @@
- Support for Arbitrary React Components and Component Props! - Support for Arbitrary React Components and Component Props!
- Autogenerated multi-level table of content and anchor links for the different headers - Autogenerated multi-level table of content and anchor links for the different headers
- Styling: [Tailwind CSS](https://tailwindcss.com/) - Styling: [Tailwind CSS](https://tailwindcss.com/)
- Good typographic defaults using [Tailwind/Typography](https://tailwindcss.com/docs/typography-plugin)
- Beside the theme declaration no CSS outside of Tailwind CSS
- Manually added support for scrollbar styling - Manually added support for scrollbar styling
- Support for [Material Icons](https://fonts.google.com/icons) - Support for [Material Icons](https://fonts.google.com/icons)
- Support for light and dark mode with a manual switch and system's selected theme by default
- Support for creating any arbitrary theming mode by swapping CSS variables - Support for creating any arbitrary theming mode by swapping CSS variables
- Support for many screen sizes and resolutions - Support for many screen sizes and resolutions
- Framework: [Next.js](https://nextjs.org/) (React)
- Multilanguage support
- State Management: [React Context](https://reactjs.org/docs/context.html) - State Management: [React Context](https://reactjs.org/docs/context.html)
- Persistent app state using LocalStorage - Persistent app state using LocalStorage
- Accessibility
- Gestures using [react-swipeable](https://www.npmjs.com/package/react-swipeable)
- Keyboard hotkeys using [react-hot-keys](https://www.npmjs.com/package/react-hot-keys)
- Support for light and dark mode with a manual switch and system's selected theme by default
- Fonts can be swaped to [OpenDyslexic](https://www.npmjs.com/package/@fontsource/opendyslexic)
- Multilingual
- By default, use the browser's language as the main language
- Fallback languages are used for content which are not available in the main language
- Main and fallback languages can be ordered manually by the user
- At the content level, the user can know which language is available
- Furthermore, the user can temporary select another language then the one that was automatically selected
- SSG + ISR (Static Site Generation + Incremental Static Regeneration): - SSG + ISR (Static Site Generation + Incremental Static Regeneration):
- The website is built before running in production - The website is built before running in production
- Performances are great, and possibility to deploy the app using a CDN - Performances are great, and possibility to deploy the app using a CDN
- On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted. - On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
- SEO - SEO
- Good defaults for the metadate and OpenGraph properties - Good defaults for the metadate and OpenGraph properties
- Each page can provide the thumbnail, title, description to be used - Each page can provide the thumbnail, title, description to be used
- Automatic generation of the sitemap using [next-sitemap](https://www.npmjs.com/package/next-sitemap) - Automatic generation of the sitemap using [next-sitemap](https://www.npmjs.com/package/next-sitemap)
- Data quality testing - Data quality testing
- Data from the CMS is subject to a battery of tests (about 20 warning types and 40 error types) at build time - Data from the CMS is subject to a battery of tests (about 20 warning types and 40 error types) at build time
- Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it. - Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it
- Check for completeness, conformity, and integrity - Check for completeness, conformity, and integrity
## Installation ## Installation

2245
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,39 +16,39 @@
"@fontsource/material-icons": "^4.5.4", "@fontsource/material-icons": "^4.5.4",
"@fontsource/material-icons-rounded": "^4.5.4", "@fontsource/material-icons-rounded": "^4.5.4",
"@fontsource/opendyslexic": "^4.5.4", "@fontsource/opendyslexic": "^4.5.4",
"@fontsource/vollkorn": "^4.5.6", "@fontsource/vollkorn": "^4.5.9",
"@fontsource/zen-maru-gothic": "^4.5.8", "@fontsource/zen-maru-gothic": "^4.5.11",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"autoprefixer": "^10.4.5", "autoprefixer": "^10.4.7",
"graphql-request": "^4.2.0", "graphql-request": "^4.2.0",
"markdown-to-jsx": "^7.1.7", "markdown-to-jsx": "^7.1.7",
"next": "^12.1.2", "next": "^12.1.6",
"nodemailer": "^6.7.3", "nodemailer": "^6.7.5",
"react": "17.0.2", "react": "18.1.0",
"react-dom": "17.0.2", "react-dom": "18.1.0",
"react-image-lightbox": "^5.1.4", "react-hot-keys": "^2.7.2",
"react-swipeable": "^6.2.1", "react-swipeable": "^7.0.0",
"turndown": "^7.1.1" "turndown": "^7.1.1"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "^2.6.2", "@graphql-codegen/cli": "^2.6.2",
"@graphql-codegen/typescript": "2.4.8", "@graphql-codegen/typescript": "2.4.11",
"@graphql-codegen/typescript-graphql-request": "^4.4.5", "@graphql-codegen/typescript-graphql-request": "^4.4.8",
"@graphql-codegen/typescript-operations": "^2.3.5", "@graphql-codegen/typescript-operations": "^2.4.0",
"@types/node": "17.0.25", "@types/node": "17.0.33",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/react": "17.0.43", "@types/react": "18.0.9",
"@types/react-dom": "^17.0.14", "@types/react-dom": "^18.0.4",
"@types/turndown": "^5.0.1", "@types/turndown": "^5.0.1",
"@typescript-eslint/eslint-plugin": "^5.20.0", "@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.20.0", "@typescript-eslint/parser": "^5.23.0",
"eslint": "^8.14.0", "eslint": "^8.15.0",
"eslint-config-next": "12.1.5", "eslint-config-next": "12.1.6",
"graphql": "^14.7.0", "graphql": "^16.5.0",
"next-sitemap": "^2.5.20", "next-sitemap": "^2.5.20",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"prettier-plugin-organize-imports": "^2.3.4", "prettier-plugin-organize-imports": "^2.3.4",
"tailwindcss": "^3.0.24", "tailwindcss": "^3.0.24",
"typescript": "^4.6.3" "typescript": "^4.6.4"
} }
} }

View File

@ -1,23 +1,19 @@
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { UploadImageFragment } from "graphql/generated"; import { UploadImageFragment } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyLanguage, prettySlug } from "helpers/formatters";
import { getOgImage, ImageQuality, OgImage } from "helpers/img";
import { Immutable } from "helpers/types";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AppStaticProps } from "queries/getAppStaticProps";
import {
getOgImage,
OgImage,
prettyLanguage,
prettySlug,
} from "queries/helpers";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import { ImageQuality } from "./Img"; import { OrderableList } from "./Inputs/OrderableList";
import OrderableList from "./Inputs/OrderableList"; import { Select } from "./Inputs/Select";
import Select from "./Inputs/Select"; import { MainPanel } from "./Panels/MainPanel";
import MainPanel from "./Panels/MainPanel"; import { Popup } from "./Popup";
import Popup from "./Popup";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
subPanel?: React.ReactNode; subPanel?: React.ReactNode;
@ -29,8 +25,19 @@ interface Props extends AppStaticProps {
description?: string; description?: string;
} }
export default function AppLayout(props: Props): JSX.Element { export function AppLayout(props: Immutable<Props>): JSX.Element {
const { langui, currencies, languages, subPanel, contentPanel } = props; const {
langui,
currencies,
languages,
subPanel,
contentPanel,
thumbnail,
title,
navTitle,
description,
subPanelIcon,
} = props;
const router = useRouter(); const router = useRouter();
const isMobile = useMediaMobile(); const isMobile = useMediaMobile();
const appLayout = useAppLayout(); const appLayout = useAppLayout();
@ -39,19 +46,23 @@ export default function AppLayout(props: Props): JSX.Element {
const handlers = useSwipeable({ const handlers = useSwipeable({
onSwipedLeft: (SwipeEventData) => { onSwipedLeft: (SwipeEventData) => {
if (SwipeEventData.velocity < sensibilitySwipe) return; if (appLayout.menuGestures) {
if (appLayout.mainPanelOpen) { if (SwipeEventData.velocity < sensibilitySwipe) return;
appLayout.setMainPanelOpen(false); if (appLayout.mainPanelOpen) {
} else if (subPanel && contentPanel) { appLayout.setMainPanelOpen(false);
appLayout.setSubPanelOpen(true); } else if (subPanel && contentPanel) {
appLayout.setSubPanelOpen(true);
}
} }
}, },
onSwipedRight: (SwipeEventData) => { onSwipedRight: (SwipeEventData) => {
if (SwipeEventData.velocity < sensibilitySwipe) return; if (appLayout.menuGestures) {
if (appLayout.subPanelOpen) { if (SwipeEventData.velocity < sensibilitySwipe) return;
appLayout.setSubPanelOpen(false); if (appLayout.subPanelOpen) {
} else { appLayout.setSubPanelOpen(false);
appLayout.setMainPanelOpen(true); } else {
appLayout.setMainPanelOpen(true);
}
} }
}, },
}); });
@ -59,8 +70,8 @@ export default function AppLayout(props: Props): JSX.Element {
const turnSubIntoContent = subPanel && !contentPanel; const turnSubIntoContent = subPanel && !contentPanel;
const titlePrefix = "Accords Library"; const titlePrefix = "Accords Library";
const metaImage: OgImage = props.thumbnail const metaImage: OgImage = thumbnail
? getOgImage(ImageQuality.Og, props.thumbnail) ? getOgImage(ImageQuality.Og, thumbnail)
: { : {
image: "/default_og.jpg", image: "/default_og.jpg",
width: 1200, width: 1200,
@ -68,9 +79,9 @@ export default function AppLayout(props: Props): JSX.Element {
alt: "Accord's Library Logo", alt: "Accord's Library Logo",
}; };
const ogTitle = const ogTitle =
props.title ?? props.navTitle ?? prettySlug(router.asPath.split("/").pop()); title ?? navTitle ?? prettySlug(router.asPath.split("/").pop());
const metaDescription = props.description ?? langui.default_description ?? ""; const metaDescription = description ?? langui.default_description ?? "";
useEffect(() => { useEffect(() => {
document.getElementsByTagName("html")[0].style.fontSize = `${ document.getElementsByTagName("html")[0].style.fontSize = `${
@ -115,7 +126,7 @@ export default function AppLayout(props: Props): JSX.Element {
}, [currencySelect]); }, [currencySelect]);
let gridCol = ""; let gridCol = "";
if (props.subPanel) { if (subPanel) {
if (appLayout.mainPanelReduced) { if (appLayout.mainPanelReduced) {
gridCol = "grid-cols-[6rem_20rem_1fr]"; gridCol = "grid-cols-[6rem_20rem_1fr]";
} else { } else {
@ -140,7 +151,9 @@ export default function AppLayout(props: Props): JSX.Element {
> >
<div <div
{...handlers} {...handlers}
className={`fixed inset-0 touch-pan-y p-0 m-0 bg-light text-black grid [grid-template-areas:'main_sub_content'] ${gridCol} mobile:grid-cols-[1fr] mobile:grid-rows-[1fr_5rem] mobile:[grid-template-areas:'content''navbar']`} className={`fixed inset-0 touch-pan-y p-0 m-0 bg-light text-black grid
[grid-template-areas:'main_sub_content'] ${gridCol} mobile:grid-cols-[1fr]
mobile:grid-rows-[1fr_5rem] mobile:[grid-template-areas:'content''navbar']`}
> >
<Head> <Head>
<title>{`${titlePrefix} - ${ogTitle}`}</title> <title>{`${titlePrefix} - ${ogTitle}`}</title>
@ -172,7 +185,8 @@ export default function AppLayout(props: Props): JSX.Element {
{/* Background when navbar is opened */} {/* Background when navbar is opened */}
<div <div
className={`[grid-area:content] mobile:z-10 absolute inset-0 transition-[backdrop-filter] duration-500 ${ className={`[grid-area:content] mobile:z-10 absolute
inset-0 transition-[backdrop-filter] duration-500 ${
(appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile (appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile
? "[backdrop-filter:blur(2px)]" ? "[backdrop-filter:blur(2px)]"
: "pointer-events-none touch-none " : "pointer-events-none touch-none "
@ -201,7 +215,10 @@ export default function AppLayout(props: Props): JSX.Element {
contentPanel contentPanel
) : ( ) : (
<div className="grid place-content-center h-full"> <div className="grid place-content-center h-full">
<div className="text-dark border-dark border-2 border-dotted rounded-2xl p-8 grid grid-flow-col place-items-center gap-9 opacity-40"> <div
className="text-dark border-dark border-2 border-dotted rounded-2xl
p-8 grid grid-flow-col place-items-center gap-9 opacity-40"
>
<p className="text-4xl"></p> <p className="text-4xl"></p>
<p className="text-2xl w-64">{langui.select_option_sidebar}</p> <p className="text-2xl w-64">{langui.select_option_sidebar}</p>
</div> </div>
@ -212,7 +229,10 @@ export default function AppLayout(props: Props): JSX.Element {
{/* Sub panel */} {/* Sub panel */}
{subPanel && ( {subPanel && (
<div <div
className={`[grid-area:sub] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%] mobile:justify-self-end border-r-[1px] mobile:border-r-0 mobile:border-l-[1px] border-black border-dotted overflow-y-scroll webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots className={`[grid-area:sub] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%]
mobile:justify-self-end border-r-[1px] mobile:border-r-0 mobile:border-l-[1px]
border-black border-dotted overflow-y-scroll webkit-scrollbar:w-0
[scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots
${ ${
turnSubIntoContent turnSubIntoContent
? "mobile:border-l-0 mobile:w-full" ? "mobile:border-l-0 mobile:w-full"
@ -225,14 +245,21 @@ export default function AppLayout(props: Props): JSX.Element {
{/* Main panel */} {/* Main panel */}
<div <div
className={`[grid-area:main] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%] mobile:justify-self-start border-r-[1px] border-black border-dotted overflow-y-scroll webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots className={`[grid-area:main] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%]
${appLayout.mainPanelOpen ? "" : "mobile:-translate-x-full"}`} mobile:justify-self-start border-r-[1px] border-black border-dotted overflow-y-scroll
webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light
texture-paper-dots ${
appLayout.mainPanelOpen ? "" : "mobile:-translate-x-full"
}`}
> >
<MainPanel langui={langui} /> <MainPanel langui={langui} />
</div> </div>
{/* Navbar */} {/* Navbar */}
<div className="[grid-area:navbar] border-t-[1px] border-black border-dotted grid grid-cols-[5rem_1fr_5rem] place-items-center desktop:hidden bg-light texture-paper-dots"> <div
className="[grid-area:navbar] border-t-[1px] border-black border-dotted grid
grid-cols-[5rem_1fr_5rem] place-items-center desktop:hidden bg-light texture-paper-dots"
>
<span <span
className="material-icons mt-[.1em] cursor-pointer" className="material-icons mt-[.1em] cursor-pointer"
onClick={() => { onClick={() => {
@ -261,8 +288,8 @@ export default function AppLayout(props: Props): JSX.Element {
{subPanel && !turnSubIntoContent {subPanel && !turnSubIntoContent
? appLayout.subPanelOpen ? appLayout.subPanelOpen
? "close" ? "close"
: props.subPanelIcon : subPanelIcon
? props.subPanelIcon ? subPanelIcon
: "tune" : "tune"
: ""} : ""}
</span> </span>
@ -274,7 +301,10 @@ export default function AppLayout(props: Props): JSX.Element {
> >
<h2 className="text-2xl">{langui.settings}</h2> <h2 className="text-2xl">{langui.settings}</h2>
<div className="mt-4 grid gap-16 justify-items-center text-center desktop:grid-cols-[auto_auto]"> <div
className="mt-4 grid gap-16 justify-items-center
text-center desktop:grid-cols-[auto_auto]"
>
{router.locales && ( {router.locales && (
<div> <div>
<h3 className="text-xl">{langui.languages}</h3> <h3 className="text-xl">{langui.languages}</h3>
@ -295,6 +325,12 @@ export default function AppLayout(props: Props): JSX.Element {
]) ])
) )
} }
insertLabels={
new Map([
[0, langui.primary_language],
[1, langui.secondary_language],
])
}
onChange={(items) => { onChange={(items) => {
const preferredLanguages = [...items].map( const preferredLanguages = [...items].map(
([code]) => code ([code]) => code
@ -437,6 +473,7 @@ export default function AppLayout(props: Props): JSX.Element {
(event.target as HTMLInputElement).value (event.target as HTMLInputElement).value
) )
} }
value={appLayout.playerName}
/> />
</div> </div>
</div> </div>

View File

@ -1,12 +1,16 @@
import { Immutable } from "helpers/types";
interface Props { interface Props {
className?: string; className?: string;
children: React.ReactNode; children: React.ReactNode;
} }
export default function Chip(props: Props): JSX.Element { export function Chip(props: Immutable<Props>): JSX.Element {
return ( return (
<div <div
className={`grid place-content-center place-items-center text-xs pb-[0.14rem] whitespace-nowrap px-1.5 border-[1px] rounded-full opacity-70 transition-[color,_opacity,_border-color] hover:opacity-100 ${props.className}`} className={`grid place-content-center place-items-center text-xs pb-[0.14rem]
whitespace-nowrap px-1.5 border-[1px] rounded-full opacity-70
transition-[color,_opacity,_border-color] hover:opacity-100 ${props.className}`}
> >
{props.children} {props.children}
</div> </div>

View File

@ -1,8 +1,10 @@
import { Immutable } from "helpers/types";
interface Props { interface Props {
className?: string; className?: string;
} }
export default function HorizontalLine(props: Props): JSX.Element { export function HorizontalLine(props: Immutable<Props>): JSX.Element {
return ( return (
<div <div
className={`h-0 w-full my-8 border-t-[3px] border-dotted border-black ${props.className}`} className={`h-0 w-full my-8 border-t-[3px] border-dotted border-black ${props.className}`}

View File

@ -1,106 +1,43 @@
import { UploadImageFragment } from "graphql/generated"; import { UploadImageFragment } from "graphql/generated";
import Image, { ImageProps } from "next/image"; import { getAssetURL, getImgSizesByQuality, ImageQuality } from "helpers/img";
import { Immutable } from "helpers/types";
import { ImageProps } from "next/image";
import { MouseEventHandler } from "react";
interface Props { interface Props {
className?: string; className?: string;
image?: UploadImageFragment | string; image?: UploadImageFragment | string;
quality?: ImageQuality; quality?: ImageQuality;
alt?: ImageProps["alt"]; alt?: ImageProps["alt"];
layout?: ImageProps["layout"]; onClick?: MouseEventHandler<HTMLImageElement>;
objectFit?: ImageProps["objectFit"];
priority?: ImageProps["priority"];
} }
export default function Img(props: Props): JSX.Element { export function Img(props: Immutable<Props>): JSX.Element {
if (typeof props.image === "string") { const {
className,
image,
quality = ImageQuality.Small,
alt,
onClick,
} = props;
if (typeof image === "string") {
return (
<img className={className} src={image} alt={alt ?? ""} loading="lazy" />
);
} else if (image?.width && image.height) {
const imgSize = getImgSizesByQuality(image.width, image.height, quality);
return ( return (
<img <img
className={props.className} className={className}
src={props.image} src={getAssetURL(image.url, quality)}
alt={props.alt ?? ""} alt={alt ?? image.alternativeText ?? ""}
/> width={imgSize.width}
); height={imgSize.height}
} else if (props.image?.width && props.image.height) { loading="lazy"
const imgSize = getImgSizesByQuality( onClick={onClick}
props.image.width,
props.image.height,
props.quality ?? ImageQuality.Small
);
return (
<Image
className={props.className}
src={getAssetURL(
props.image.url,
props.quality ? props.quality : ImageQuality.Small
)}
alt={props.alt ?? props.image.alternativeText ?? ""}
width={props.layout === "fill" ? undefined : imgSize.width}
height={props.layout === "fill" ? undefined : imgSize.height}
layout={props.layout}
objectFit={props.objectFit}
priority={props.priority}
unoptimized
/> />
); );
} }
return <></>; return <></>;
} }
export enum ImageQuality {
Small = "small",
Medium = "medium",
Large = "large",
Og = "og",
}
export function getAssetFilename(path: string): string {
let result = path.split("/");
result = result[result.length - 1].split(".");
result = result
.splice(0, result.length - 1)
.join(".")
.split("_");
return result[0];
}
export function getAssetURL(url: string, quality: ImageQuality): string {
let newUrl = url;
newUrl = newUrl.replace(/^\/uploads/u, `/${quality}`);
newUrl = newUrl.replace(/.jpg$/u, ".webp");
newUrl = newUrl.replace(/.jpeg$/u, ".webp");
newUrl = newUrl.replace(/.png$/u, ".webp");
if (quality === ImageQuality.Og) newUrl = newUrl.replace(/.webp$/u, ".jpg");
return process.env.NEXT_PUBLIC_URL_IMG + newUrl;
}
export function getImgSizesByMaxSize(
width: number,
height: number,
maxSize: number
): { width: number; height: number } {
if (width > height) {
if (width < maxSize) return { width: width, height: height };
return { width: maxSize, height: (height / width) * maxSize };
}
if (height < maxSize) return { width: width, height: height };
return { width: (width / height) * maxSize, height: maxSize };
}
export function getImgSizesByQuality(
width: number,
height: number,
quality: ImageQuality
): { width: number; height: number } {
switch (quality) {
case ImageQuality.Og:
return getImgSizesByMaxSize(width, height, 512);
case ImageQuality.Small:
return getImgSizesByMaxSize(width, height, 512);
case ImageQuality.Medium:
return getImgSizesByMaxSize(width, height, 1024);
case ImageQuality.Large:
return getImgSizesByMaxSize(width, height, 2048);
default:
return { width: 0, height: 0 };
}
}

View File

@ -1,3 +1,4 @@
import { Immutable } from "helpers/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { MouseEventHandler } from "react"; import { MouseEventHandler } from "react";
@ -14,7 +15,7 @@ interface Props {
badgeNumber?: number; badgeNumber?: number;
} }
export default function Button(props: Props): JSX.Element { export function Button(props: Immutable<Props>): JSX.Element {
const { const {
draggable, draggable,
id, id,
@ -39,11 +40,15 @@ export default function Button(props: Props): JSX.Element {
transition-all select-none hover:[--opacityBadge:0] --opacityBadge:100 ${className} ${ transition-all select-none hover:[--opacityBadge:0] --opacityBadge:100 ${className} ${
active active
? "text-light bg-black drop-shadow-black-lg !border-black cursor-not-allowed" ? "text-light bg-black drop-shadow-black-lg !border-black cursor-not-allowed"
: "cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg active:bg-black active:text-light active:drop-shadow-black-lg active:border-black" : `cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg
active:bg-black active:text-light active:drop-shadow-black-lg active:border-black`
}`} }`}
> >
{badgeNumber && ( {badgeNumber && (
<div className="opacity-[var(--opacityBadge)] transition-opacity grid place-items-center absolute -top-3 -right-2 bg-dark w-8 h-8 text-light font-bold rounded-full"> <div
className="opacity-[var(--opacityBadge)] transition-opacity grid place-items-center
absolute -top-3 -right-2 bg-dark w-8 h-8 text-light font-bold rounded-full"
>
{badgeNumber} {badgeNumber}
</div> </div>
)} )}

View File

@ -1,8 +1,9 @@
import { AppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyLanguage } from "queries/helpers"; import { prettyLanguage } from "helpers/formatters";
import { Immutable } from "helpers/types";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
import ToolTip from "../ToolTip"; import { ToolTip } from "../ToolTip";
import Button from "./Button"; import { Button } from "./Button";
interface Props { interface Props {
className?: string; className?: string;
@ -12,7 +13,7 @@ interface Props {
setLocalesIndex: Dispatch<SetStateAction<number | undefined>>; setLocalesIndex: Dispatch<SetStateAction<number | undefined>>;
} }
export default function LanguageSwitcher(props: Props): JSX.Element { export function LanguageSwitcher(props: Immutable<Props>): JSX.Element {
const { locales, className, localesIndex, setLocalesIndex } = props; const { locales, className, localesIndex, setLocalesIndex } = props;
return ( return (

View File

@ -1,13 +1,15 @@
import { arrayMove } from "queries/helpers"; import { arrayMove } from "helpers/others";
import { Immutable } from "helpers/types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
interface Props { interface Props {
className?: string; className?: string;
items: Map<string, string>; items: Map<string, string>;
insertLabels?: Map<number, string | null | undefined>;
onChange?: (items: Map<string, string>) => void; onChange?: (items: Map<string, string>) => void;
} }
export default function OrderableList(props: Props): JSX.Element { export function OrderableList(props: Immutable<Props>): JSX.Element {
const [items, setItems] = useState<Map<string, string>>(props.items); const [items, setItems] = useState<Map<string, string>>(props.items);
useEffect(() => { useEffect(() => {
@ -24,12 +26,8 @@ export default function OrderableList(props: Props): JSX.Element {
<div className="grid gap-2"> <div className="grid gap-2">
{[...items].map(([key, value], index) => ( {[...items].map(([key, value], index) => (
<> <>
{index === 0 ? ( {props.insertLabels?.get(index) && (
<p>Primary language</p> <p>{props.insertLabels.get(index)}</p>
) : index === 1 ? (
<p>Secondary languages</p>
) : (
""
)} )}
<div <div
onDragStart={(event) => { onDragStart={(event) => {

View File

@ -1,5 +1,6 @@
import { Immutable } from "helpers/types";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
import Button from "./Button"; import { Button } from "./Button";
interface Props { interface Props {
className?: string; className?: string;
@ -8,7 +9,7 @@ interface Props {
setPage: Dispatch<SetStateAction<number>>; setPage: Dispatch<SetStateAction<number>>;
} }
export default function PageSelector(props: Props): JSX.Element { export function PageSelector(props: Immutable<Props>): JSX.Element {
const { page, setPage, maxPage } = props; const { page, setPage, maxPage } = props;
return ( return (

View File

@ -1,3 +1,4 @@
import { Immutable } from "helpers/types";
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
interface Props { interface Props {
@ -9,7 +10,7 @@ interface Props {
className?: string; className?: string;
} }
export default function Select(props: Props): JSX.Element { export function Select(props: Immutable<Props>): JSX.Element {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
return ( return (
@ -19,7 +20,9 @@ export default function Select(props: Props): JSX.Element {
} ${props.className}`} } ${props.className}`}
> >
<div <div
className={`outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent] bg-light rounded-[1em] p-1 grid grid-flow-col grid-cols-[1fr_auto_auto] place-items-center cursor-pointer hover:bg-mid transition-all ${ className={`outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent]
bg-light rounded-[1em] p-1 grid grid-flow-col grid-cols-[1fr_auto_auto] place-items-center
cursor-pointer hover:bg-mid transition-all ${
opened && "outline-[transparent] rounded-b-none" opened && "outline-[transparent] rounded-b-none"
}`} }`}
> >
@ -47,7 +50,8 @@ export default function Select(props: Props): JSX.Element {
<> <>
{index !== props.state && ( {index !== props.state && (
<div <div
className="bg-light hover:bg-mid transition-colors cursor-pointer p-1 last-of-type:rounded-b-[1em]" className="bg-light hover:bg-mid transition-colors
cursor-pointer p-1 last-of-type:rounded-b-[1em]"
key={index} key={index}
id={option} id={option}
onClick={() => { onClick={() => {

View File

@ -1,3 +1,4 @@
import { Immutable } from "helpers/types";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
interface Props { interface Props {
@ -6,18 +7,20 @@ interface Props {
className?: string; className?: string;
} }
export default function Switch(props: Props): JSX.Element { export function Switch(props: Immutable<Props>): JSX.Element {
return ( return (
<div <div
className={`h-6 w-12 rounded-full border-2 border-mid grid transition-colors relative cursor-pointer ${ className={`h-6 w-12 rounded-full border-2 border-mid grid
props.className transition-colors relative cursor-pointer ${props.className} ${
} ${props.state ? "bg-mid" : "bg-light"}`} props.state ? "bg-mid" : "bg-light"
}`}
onClick={() => { onClick={() => {
props.setState(!props.state); props.setState(!props.state);
}} }}
> >
<div <div
className={`bg-dark aspect-square rounded-full absolute top-0 bottom-0 left-0 transition-transform ${ className={`bg-dark aspect-square rounded-full absolute
top-0 bottom-0 left-0 transition-transform ${
props.state && "translate-x-[115%]" props.state && "translate-x-[115%]"
}`} }`}
></div> ></div>

View File

@ -1,10 +1,12 @@
import { Immutable } from "helpers/types";
interface Props { interface Props {
className?: string; className?: string;
children: React.ReactNode; children: React.ReactNode;
id?: string; id?: string;
} }
export default function InsetBox(props: Props): JSX.Element { export function InsetBox(props: Immutable<Props>): JSX.Element {
return ( return (
<div <div
id={props.id} id={props.id}

View File

@ -1,26 +1,24 @@
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { GetLibraryItemQuery } from "graphql/generated"; import { GetLibraryItemQuery } from "graphql/generated";
import { AppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyinlineTitle, prettySlug } from "queries/helpers"; import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { Immutable } from "helpers/types";
import { useState } from "react"; import { useState } from "react";
interface Props { interface Props {
content: Exclude< content: NonNullable<
Exclude< NonNullable<
Exclude< NonNullable<
GetLibraryItemQuery["libraryItems"], GetLibraryItemQuery["libraryItems"]
null | undefined >["data"][number]["attributes"]
>["data"][number]["attributes"], >["contents"]
null | undefined
>["contents"],
null | undefined
>["data"][number]; >["data"][number];
parentSlug: string; parentSlug: string;
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
} }
export default function ContentLine(props: Props): JSX.Element { export function ContentLine(props: Immutable<Props>): JSX.Element {
const { content, langui, parentSlug } = props; const { content, langui, parentSlug } = props;
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
@ -32,15 +30,19 @@ export default function ContentLine(props: Props): JSX.Element {
opened && "bg-mid shadow-inner-sm shadow-shade h-auto py-3 my-2" opened && "bg-mid shadow-inner-sm shadow-shade h-auto py-3 my-2"
}`} }`}
> >
<div className="grid gap-4 place-items-center grid-cols-[auto_auto_1fr_auto_12ch] thin:grid-cols-[auto_auto_1fr_auto]"> <div
className="grid gap-4 place-items-center
grid-cols-[auto_auto_1fr_auto_12ch] thin:grid-cols-[auto_auto_1fr_auto]"
>
<a> <a>
<h3 className="cursor-pointer" onClick={() => setOpened(!opened)}> <h3 className="cursor-pointer" onClick={() => setOpened(!opened)}>
{content.attributes.content?.data?.attributes?.titles?.[0] {content.attributes.content?.data?.attributes?.translations?.[0]
? prettyinlineTitle( ? prettyinlineTitle(
content.attributes.content.data.attributes.titles[0] content.attributes.content.data.attributes.translations[0]
?.pre_title, ?.pre_title,
content.attributes.content.data.attributes.titles[0]?.title, content.attributes.content.data.attributes.translations[0]
content.attributes.content.data.attributes.titles[0] ?.title,
content.attributes.content.data.attributes.translations[0]
?.subtitle ?.subtitle
) )
: prettySlug(content.attributes.slug, props.parentSlug)} : prettySlug(content.attributes.slug, props.parentSlug)}

View File

@ -1,76 +1,55 @@
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Img, { import { Img } from "components/Img";
getAssetFilename, import { Button } from "components/Inputs/Button";
getAssetURL, import { RecorderChip } from "components/RecorderChip";
ImageQuality, import { ToolTip } from "components/ToolTip";
} from "components/Img";
import Button from "components/Inputs/Button";
import RecorderChip from "components/RecorderChip";
import ToolTip from "components/ToolTip";
import { GetLibraryItemScansQuery } from "graphql/generated"; import { GetLibraryItemScansQuery } from "graphql/generated";
import useSmartLanguage from "hooks/useSmartLanguage"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { AppStaticProps } from "queries/getAppStaticProps"; import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img";
import { getStatusDescription, isInteger } from "queries/helpers"; import { isInteger } from "helpers/numbers";
import { Dispatch, SetStateAction } from "react"; import { getStatusDescription } from "helpers/others";
import { Immutable } from "helpers/types";
import { useSmartLanguage } from "hooks/useSmartLanguage";
interface Props { interface Props {
setLightboxOpen: Dispatch<SetStateAction<boolean>>; openLightBox: (images: string[], index?: number) => void;
setLightboxImages: Dispatch<SetStateAction<string[]>>; scanSet: NonNullable<
setLightboxIndex: Dispatch<SetStateAction<number>>; NonNullable<
scanSet: Exclude< NonNullable<
Exclude< NonNullable<
Exclude< NonNullable<
Exclude< GetLibraryItemScansQuery["libraryItems"]
Exclude< >["data"][number]["attributes"]
GetLibraryItemScansQuery["libraryItems"], >["contents"]
null | undefined >["data"][number]["attributes"]
>["data"][number]["attributes"], >["scan_set"]
null | undefined
>["contents"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>["scan_set"],
null | undefined
>; >;
slug: string; slug: string;
title: string; title: string;
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
content: Exclude< content: NonNullable<
Exclude< NonNullable<
Exclude< NonNullable<
Exclude< NonNullable<
GetLibraryItemScansQuery["libraryItems"], GetLibraryItemScansQuery["libraryItems"]
null | undefined >["data"][number]["attributes"]
>["data"][number]["attributes"], >["contents"]
null | undefined >["data"][number]["attributes"]
>["contents"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>["content"]; >["content"];
} }
export default function ScanSet(props: Props): JSX.Element { export function ScanSet(props: Immutable<Props>): JSX.Element {
const { const { openLightBox, scanSet, slug, title, languages, langui, content } =
setLightboxOpen, props;
setLightboxImages,
setLightboxIndex,
scanSet,
slug,
title,
languages,
langui,
content,
} = props;
const [selectedScan, LanguageSwitcher] = useSmartLanguage({ const [selectedScan, LanguageSwitcher] = useSmartLanguage({
items: scanSet, items: scanSet,
languages: languages, languages: languages,
languageExtractor: (item) => item?.language?.data?.attributes?.code, languageExtractor: (item) => item.language?.data?.attributes?.code,
transform: (item) => { transform: (item) => {
item?.pages?.data.sort((a, b) => { const newItem = { ...item } as NonNullable<Props["scanSet"][number]>;
newItem.pages?.data.sort((a, b) => {
if (a.attributes?.url && b.attributes?.url) { if (a.attributes?.url && b.attributes?.url) {
let aName = getAssetFilename(a.attributes.url); let aName = getAssetFilename(a.attributes.url);
let bName = getAssetFilename(b.attributes.url); let bName = getAssetFilename(b.attributes.url);
@ -93,7 +72,7 @@ export default function ScanSet(props: Props): JSX.Element {
} }
return 0; return 0;
}); });
return item; return newItem;
}, },
}); });
@ -101,7 +80,10 @@ export default function ScanSet(props: Props): JSX.Element {
<> <>
{selectedScan && ( {selectedScan && (
<div> <div>
<div className="flex flex-row flex-wrap place-items-center gap-6 text-base pt-10 first-of-type:pt-0"> <div
className="flex flex-row flex-wrap place-items-center
gap-6 text-base pt-10 first-of-type:pt-0"
>
<h2 id={slug} className="text-2xl"> <h2 id={slug} className="text-2xl">
{title} {title}
</h2> </h2>
@ -198,11 +180,16 @@ export default function ScanSet(props: Props): JSX.Element {
)} )}
</div> </div>
<div className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"> <div
className="grid gap-8 items-end mobile:grid-cols-2
desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))]
pb-12 border-b-[3px] border-dotted last-of-type:border-0"
>
{selectedScan.pages?.data.map((page, index) => ( {selectedScan.pages?.data.map((page, index) => (
<div <div
key={page.id} key={page.id}
className="drop-shadow-shade-lg hover:scale-[1.02] cursor-pointer transition-transform" className="drop-shadow-shade-lg hover:scale-[1.02]
cursor-pointer transition-transform"
onClick={() => { onClick={() => {
const images: string[] = []; const images: string[] = [];
selectedScan.pages?.data.map((image) => { selectedScan.pages?.data.map((image) => {
@ -211,9 +198,7 @@ export default function ScanSet(props: Props): JSX.Element {
getAssetURL(image.attributes.url, ImageQuality.Large) getAssetURL(image.attributes.url, ImageQuality.Large)
); );
}); });
setLightboxOpen(true); openLightBox(images, index);
setLightboxImages(images);
setLightboxIndex(index);
}} }}
> >
{page.attributes && ( {page.attributes && (

View File

@ -1,48 +1,37 @@
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Img, { getAssetURL, ImageQuality } from "components/Img"; import { Img } from "components/Img";
import RecorderChip from "components/RecorderChip"; import { RecorderChip } from "components/RecorderChip";
import ToolTip from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { import {
GetLibraryItemScansQuery, GetLibraryItemScansQuery,
UploadImageFragment, UploadImageFragment,
} from "graphql/generated"; } from "graphql/generated";
import useSmartLanguage from "hooks/useSmartLanguage"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { AppStaticProps } from "queries/getAppStaticProps"; import { getAssetURL, ImageQuality } from "helpers/img";
import { getStatusDescription } from "queries/helpers"; import { getStatusDescription } from "helpers/others";
import { Dispatch, SetStateAction } from "react"; import { Immutable } from "helpers/types";
import { useSmartLanguage } from "hooks/useSmartLanguage";
interface Props { interface Props {
setLightboxOpen: Dispatch<SetStateAction<boolean>>; openLightBox: (images: string[], index?: number) => void;
setLightboxImages: Dispatch<SetStateAction<string[]>>; images: NonNullable<
setLightboxIndex: Dispatch<SetStateAction<number>>; NonNullable<
images: Exclude< NonNullable<
Exclude< GetLibraryItemScansQuery["libraryItems"]
Exclude< >["data"][number]["attributes"]
GetLibraryItemScansQuery["libraryItems"], >["images"]
null | undefined
>["data"][number]["attributes"],
null | undefined
>["images"],
null | undefined
>; >;
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
} }
export default function ScanSetCover(props: Props): JSX.Element { export function ScanSetCover(props: Immutable<Props>): JSX.Element {
const { const { openLightBox, images, languages, langui } = props;
setLightboxOpen,
setLightboxImages,
setLightboxIndex,
images,
languages,
langui,
} = props;
const [selectedScan, LanguageSwitcher] = useSmartLanguage({ const [selectedScan, LanguageSwitcher] = useSmartLanguage({
items: images, items: images,
languages: languages, languages: languages,
languageExtractor: (item) => item?.language?.data?.attributes?.code, languageExtractor: (item) => item.language?.data?.attributes?.code,
}); });
const coverImages: UploadImageFragment[] = []; const coverImages: UploadImageFragment[] = [];
@ -64,7 +53,10 @@ export default function ScanSetCover(props: Props): JSX.Element {
<> <>
{selectedScan && ( {selectedScan && (
<div> <div>
<div className="flex flex-row flex-wrap place-items-center gap-6 text-base pt-10 first-of-type:pt-0"> <div
className="flex flex-row flex-wrap place-items-center
gap-6 text-base pt-10 first-of-type:pt-0"
>
<h2 id={"cover"} className="text-2xl"> <h2 id={"cover"} className="text-2xl">
{"Cover"} {"Cover"}
</h2> </h2>
@ -149,20 +141,23 @@ export default function ScanSetCover(props: Props): JSX.Element {
)} )}
</div> </div>
<div className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"> <div
className="grid gap-8 items-end mobile:grid-cols-2
desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))]
pb-12 border-b-[3px] border-dotted last-of-type:border-0"
>
{coverImages.map((image, index) => ( {coverImages.map((image, index) => (
<div <div
key={image.url} key={image.url}
className="drop-shadow-shade-lg hover:scale-[1.02] cursor-pointer transition-transform" className="drop-shadow-shade-lg hover:scale-[1.02]
cursor-pointer transition-transform"
onClick={() => { onClick={() => {
const imgs: string[] = []; const imgs: string[] = [];
coverImages.map((img) => { coverImages.map((img) => {
if (img.url) if (img.url)
imgs.push(getAssetURL(img.url, ImageQuality.Large)); imgs.push(getAssetURL(img.url, ImageQuality.Large));
}); });
setLightboxOpen(true); openLightBox(imgs, index);
setLightboxImages(imgs);
setLightboxIndex(index);
}} }}
> >
<Img image={image} quality={ImageQuality.Small} /> <Img image={image} quality={ImageQuality.Small} />

View File

@ -1,6 +1,10 @@
import { useMediaMobile } from "hooks/useMediaQuery"; import { Immutable } from "helpers/types";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
import Lightbox from "react-image-lightbox"; import Hotkeys from "react-hot-keys";
import { useSwipeable } from "react-swipeable";
import { Img } from "./Img";
import { Button } from "./Inputs/Button";
import { Popup } from "./Popup";
interface Props { interface Props {
setState: setState:
@ -12,27 +16,75 @@ interface Props {
setIndex: Dispatch<SetStateAction<number>>; setIndex: Dispatch<SetStateAction<number>>;
} }
export default function LightBox(props: Props): JSX.Element { export function LightBox(props: Immutable<Props>): JSX.Element {
const { state, setState, images, index, setIndex } = props; const { state, setState, images, index, setIndex } = props;
const mobile = useMediaMobile();
function handlePrevious() {
if (index > 0) setIndex(index - 1);
}
function handleNext() {
if (index < images.length - 1) setIndex(index + 1);
}
const sensibilitySwipe = 0.5;
const handlers = useSwipeable({
onSwipedLeft: (SwipeEventData) => {
if (SwipeEventData.velocity < sensibilitySwipe) return;
handleNext();
},
onSwipedRight: (SwipeEventData) => {
if (SwipeEventData.velocity < sensibilitySwipe) return;
handlePrevious();
},
});
return ( return (
<> <>
{state && ( {state && (
<Lightbox <Hotkeys
reactModalProps={{ keyName="left,right"
parentSelector: () => document.getElementById("MyAppLayout"), allowRepeat
onKeyDown={(keyName) => {
if (keyName === "left") {
handlePrevious();
} else {
handleNext();
}
}} }}
mainSrc={images[index]} >
prevSrc={index > 0 ? images[index - 1] : undefined} <Popup setState={setState} state={state} padding={false} fillViewport>
nextSrc={index < images.length ? images[index + 1] : undefined} <div
onMovePrevRequest={() => setIndex(index - 1)} {...handlers}
onMoveNextRequest={() => setIndex(index + 1)} className={`grid grid-cols-[4em,1fr,4em] mobile:grid-cols-2
imageCaption="" [grid-template-areas:"left_image_right"]
imageTitle="" mobile:[grid-template-areas:"image_image""left_right"]
onCloseRequest={() => setState(false)} place-items-center first-letter:gap-4 w-full h-full overflow-hidden`}
imagePadding={mobile ? 0 : 70} >
/> <div className="[grid-area:left]">
{index > 0 && (
<Button onClick={handlePrevious}>
<span className="material-icons">chevron_left</span>
</Button>
)}
</div>
<Img
className="max-h-full [grid-area:image]"
image={images[index]}
/>
<div className="[grid-area:right]">
{index < images.length - 1 && (
<Button onClick={handleNext}>
<span className="material-icons">chevron_right</span>
</Button>
)}
</div>
</div>
</Popup>
</Hotkeys>
)} )}
</> </>
); );

View File

@ -1,13 +1,15 @@
import HorizontalLine from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import Img, { getAssetURL, ImageQuality } from "components/Img"; import { Img } from "components/Img";
import InsetBox from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import LightBox from "components/LightBox"; import { ToolTip } from "components/ToolTip";
import ToolTip from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img";
import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox";
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { slugify } from "queries/helpers"; import React from "react";
import React, { useState } from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
interface Props { interface Props {
@ -15,26 +17,18 @@ interface Props {
text: string; text: string;
} }
export default function Markdawn(props: Props): JSX.Element { export function Markdawn(props: Immutable<Props>): JSX.Element {
const appLayout = useAppLayout(); const appLayout = useAppLayout();
const text = preprocessMarkDawn(props.text); const text = preprocessMarkDawn(props.text);
const router = useRouter(); const router = useRouter();
const [lightboxOpen, setLightboxOpen] = useState(false); const [openLightBox, LightBox] = useLightBox();
const [lightboxImages, setLightboxImages] = useState([""]);
const [lightboxIndex, setLightboxIndex] = useState(0);
if (text) { if (text) {
return ( return (
<> <>
<LightBox <LightBox />
state={lightboxOpen}
setState={setLightboxOpen}
images={lightboxImages}
index={lightboxIndex}
setIndex={setLightboxIndex}
/>
<Markdown <Markdown
className={`formatted ${props.className}`} className={`formatted ${props.className}`}
options={{ options={{
@ -253,13 +247,11 @@ export default function Markdawn(props: Props): JSX.Element {
<div <div
className="my-8 cursor-pointer place-content-center grid" className="my-8 cursor-pointer place-content-center grid"
onClick={() => { onClick={() => {
setLightboxOpen(true); openLightBox([
setLightboxImages([
compProps.src.startsWith("/uploads/") compProps.src.startsWith("/uploads/")
? getAssetURL(compProps.src, ImageQuality.Large) ? getAssetURL(compProps.src, ImageQuality.Large)
: compProps.src, : compProps.src,
]); ]);
setLightboxIndex(0);
}} }}
> >
<Img <Img
@ -268,8 +260,6 @@ export default function Markdawn(props: Props): JSX.Element {
? getAssetURL(compProps.src, ImageQuality.Small) ? getAssetURL(compProps.src, ImageQuality.Small)
: compProps.src : compProps.src
} }
layout="fill"
objectFit="contain"
quality={ImageQuality.Medium} quality={ImageQuality.Medium}
></Img> ></Img>
</div> </div>

View File

@ -1,5 +1,6 @@
import { slugify } from "helpers/formatters";
import { Immutable } from "helpers/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { slugify } from "queries/helpers";
import { preprocessMarkDawn } from "./Markdawn"; import { preprocessMarkDawn } from "./Markdawn";
interface Props { interface Props {
@ -7,7 +8,7 @@ interface Props {
title?: string; title?: string;
} }
export default function TOCComponent(props: Props): JSX.Element { export function TOC(props: Immutable<Props>): JSX.Element {
const { text, title } = props; const { text, title } = props;
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title); const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
const router = useRouter(); const router = useRouter();
@ -28,7 +29,7 @@ export default function TOCComponent(props: Props): JSX.Element {
} }
interface LevelProps { interface LevelProps {
tocchildren: TOC[]; tocchildren: TOCInterface[];
parentNumbering: string; parentNumbering: string;
} }
@ -60,14 +61,14 @@ function TOCLevel(props: LevelProps): JSX.Element {
); );
} }
interface TOC { interface TOCInterface {
title: string; title: string;
slug: string; slug: string;
children: TOC[]; children: TOCInterface[];
} }
export function getTocFromMarkdawn(text: string, title?: string): TOC { export function getTocFromMarkdawn(text: string, title?: string): TOCInterface {
const toc: TOC = { const toc: TOCInterface = {
title: title ?? "Return to top", title: title ?? "Return to top",
slug: slugify(title), slug: slugify(title),
children: [], children: [],

View File

@ -1,4 +1,5 @@
import ToolTip from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { Immutable } from "helpers/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { MouseEventHandler } from "react"; import { MouseEventHandler } from "react";
@ -12,15 +13,19 @@ interface Props {
onClick?: MouseEventHandler<HTMLDivElement>; onClick?: MouseEventHandler<HTMLDivElement>;
} }
export default function NavOption(props: Props): JSX.Element { export function NavOption(props: Immutable<Props>): JSX.Element {
const router = useRouter(); const router = useRouter();
const isActive = router.asPath.startsWith(props.url); const isActive = router.asPath.startsWith(props.url);
const divActive = "bg-mid shadow-inner-sm shadow-shade"; const divActive = "bg-mid shadow-inner-sm shadow-shade";
const border = const border =
"outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent]"; "outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent]";
const divCommon = `gap-x-5 w-full rounded-2xl cursor-pointer p-4 hover:bg-mid hover:shadow-inner-sm hover:shadow-shade hover:active:shadow-inner hover:active:shadow-shade transition-all ${
props.border ? border : "" const divCommon = `gap-x-5 w-full rounded-2xl cursor-pointer p-4 hover:bg-mid
} ${isActive ? divActive : ""}`; hover:shadow-inner-sm hover:shadow-shade hover:active:shadow-inner
hover:active:shadow-shade transition-all ${props.border ? border : ""} ${
isActive ? divActive : ""
}`;
return ( return (
<ToolTip <ToolTip

View File

@ -1,4 +1,5 @@
import HorizontalLine from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { Immutable } from "helpers/types";
interface Props { interface Props {
icon?: string; icon?: string;
@ -6,7 +7,7 @@ interface Props {
description?: string | null | undefined; description?: string | null | undefined;
} }
export default function PanelHeader(props: Props): JSX.Element { export function PanelHeader(props: Immutable<Props>): JSX.Element {
return ( return (
<> <>
<div className="w-full grid place-items-center"> <div className="w-full grid place-items-center">

View File

@ -1,7 +1,8 @@
import HorizontalLine from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
interface Props { interface Props {
href: string; href: string;
@ -18,7 +19,7 @@ export enum ReturnButtonType {
both = "both", both = "both",
} }
export default function ReturnButton(props: Props): JSX.Element { export function ReturnButton(props: Immutable<Props>): JSX.Element {
const appLayout = useAppLayout(); const appLayout = useAppLayout();
return ( return (

View File

@ -1,3 +1,5 @@
import { Immutable } from "helpers/types";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
autoformat?: boolean; autoformat?: boolean;
@ -9,13 +11,13 @@ export enum ContentPanelWidthSizes {
large = "large", large = "large",
} }
export default function ContentPanel(props: Props): JSX.Element { export function ContentPanel(props: Immutable<Props>): JSX.Element {
const width = props.width ? props.width : ContentPanelWidthSizes.default; const width = props.width ? props.width : ContentPanelWidthSizes.default;
const widthCSS = const widthCSS =
width === ContentPanelWidthSizes.default ? "max-w-2xl" : "w-full"; width === ContentPanelWidthSizes.default ? "max-w-2xl" : "w-full";
return ( return (
<div className={`grid pt-10 pb-20 px-6 desktop:py-20 desktop:px-10`}> <div className={`grid pt-10 pb-20 px-4 desktop:py-20 desktop:px-10`}>
<main <main
className={`${ className={`${
props.autoformat && "formatted" props.autoformat && "formatted"

View File

@ -1,25 +1,27 @@
import HorizontalLine from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import NavOption from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
import ToolTip from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { useMediaDesktop } from "hooks/useMediaQuery"; import { useMediaDesktop } from "hooks/useMediaQuery";
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx";
import Link from "next/link"; import Link from "next/link";
import { AppStaticProps } from "queries/getAppStaticProps";
interface Props { interface Props {
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
} }
export default function MainPanel(props: Props): JSX.Element { export function MainPanel(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const isDesktop = useMediaDesktop(); const isDesktop = useMediaDesktop();
const appLayout = useAppLayout(); const appLayout = useAppLayout();
return ( return (
<div <div
className={`flex flex-col justify-center content-start gap-y-2 justify-items-center text-center p-8 ${ className={`flex flex-col justify-center content-start
gap-y-2 justify-items-center text-center p-8 ${
appLayout.mainPanelReduced && isDesktop && "px-4" appLayout.mainPanelReduced && isDesktop && "px-4"
}`} }`}
> >
@ -44,7 +46,9 @@ export default function MainPanel(props: Props): JSX.Element {
onClick={() => appLayout.setMainPanelOpen(false)} onClick={() => appLayout.setMainPanelOpen(false)}
className={`${ className={`${
appLayout.mainPanelReduced && isDesktop ? "w-12" : "w-1/2" appLayout.mainPanelReduced && isDesktop ? "w-12" : "w-1/2"
} aspect-square cursor-pointer transition-colors [mask:url('/icons/accords.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black hover:bg-dark mb-4`} } aspect-square cursor-pointer transition-colors [mask:url('/icons/accords.svg')]
![mask-size:contain] ![mask-repeat:no-repeat]
![mask-position:center] bg-black hover:bg-dark mb-4`}
></div> ></div>
</Link> </Link>
@ -68,22 +72,11 @@ export default function MainPanel(props: Props): JSX.Element {
disabled={!appLayout.mainPanelReduced} disabled={!appLayout.mainPanelReduced}
> >
<Button <Button
className={
appLayout.mainPanelReduced && isDesktop
? ""
: "!py-0.5 !px-2.5"
}
onClick={() => { onClick={() => {
appLayout.setConfigPanelOpen(true); appLayout.setConfigPanelOpen(true);
}} }}
> >
<span <span className={"material-icons"}>settings</span>
className={`material-icons ${
!(appLayout.mainPanelReduced && isDesktop) && "!text-sm"
} `}
>
settings
</span>
</Button> </Button>
</ToolTip> </ToolTip>
@ -218,10 +211,22 @@ export default function MainPanel(props: Props): JSX.Element {
className="transition-[filter] colorize-black hover:colorize-dark" className="transition-[filter] colorize-black hover:colorize-dark"
href="https://creativecommons.org/licenses/by-sa/4.0/" href="https://creativecommons.org/licenses/by-sa/4.0/"
> >
<div className="mt-4 mb-8 grid grid-flow-col place-content-center gap-1 hover:[--theme-color-black:var(--theme-color-dark)]"> <div
<div className="w-6 aspect-square [mask:url('/icons/creative-commons-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" /> className="mt-4 mb-8 grid grid-flow-col place-content-center gap-1
<div className="w-6 aspect-square [mask:url('/icons/creative-commons-by-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" /> hover:[--theme-color-black:var(--theme-color-dark)]"
<div className="w-6 aspect-square [mask:url('/icons/creative-commons-sa-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" /> >
<div
className="w-6 aspect-square [mask:url('/icons/creative-commons-brands.svg')]
![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black"
/>
<div
className="w-6 aspect-square [mask:url('/icons/creative-commons-by-brands.svg')]
![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black"
/>
<div
className="w-6 aspect-square [mask:url('/icons/creative-commons-sa-brands.svg')]
![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black"
/>
</div> </div>
</a> </a>
<p> <p>
@ -232,14 +237,18 @@ export default function MainPanel(props: Props): JSX.Element {
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8"> <div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">
<a <a
aria-label="Browse our GitHub repository, which include this website source code" aria-label="Browse our GitHub repository, which include this website source code"
className="transition-colors [mask:url('/icons/github-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] w-10 aspect-square bg-black hover:bg-dark" className="transition-colors [mask:url('/icons/github-brands.svg')]
![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center]
w-10 aspect-square bg-black hover:bg-dark"
href="https://github.com/Accords-Library" href="https://github.com/Accords-Library"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
></a> ></a>
<a <a
aria-label="Join our Discord server!" aria-label="Join our Discord server!"
className="transition-colors [mask:url('/icons/discord-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] w-10 aspect-square bg-black hover:bg-dark" className="transition-colors [mask:url('/icons/discord-brands.svg')]
![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center]
w-10 aspect-square bg-black hover:bg-dark"
href="/discord" href="/discord"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -1,8 +1,10 @@
import { Immutable } from "helpers/types";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
} }
export default function SubPanel(props: Props): JSX.Element { export function SubPanel(props: Immutable<Props>): JSX.Element {
return ( return (
<div className="grid pt-10 pb-20 px-6 desktop:py-8 desktop:px-10 gap-y-2 text-center"> <div className="grid pt-10 pb-20 px-6 desktop:py-8 desktop:px-10 gap-y-2 text-center">
{props.children} {props.children}

View File

@ -1,4 +1,7 @@
import { Dispatch, SetStateAction } from "react"; import { useAppLayout } from "contexts/AppLayoutContext";
import { Immutable } from "helpers/types";
import { Dispatch, SetStateAction, useEffect } from "react";
import Hotkeys from "react-hot-keys";
interface Props { interface Props {
setState: setState:
@ -8,42 +11,65 @@ interface Props {
children: React.ReactNode; children: React.ReactNode;
fillViewport?: boolean; fillViewport?: boolean;
hideBackground?: boolean; hideBackground?: boolean;
padding?: boolean;
} }
export default function Popup(props: Props): JSX.Element { export function Popup(props: Immutable<Props>): JSX.Element {
const {
setState,
state,
children,
fillViewport,
hideBackground,
padding = true,
} = props;
const appLayout = useAppLayout();
useEffect(() => {
appLayout.setMenuGestures(!state);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
return ( return (
<div <Hotkeys
className={`fixed inset-0 z-50 grid place-content-center keyName="escape"
transition-[backdrop-filter] duration-500 ${ allowRepeat
props.state onKeyDown={() => {
? "[backdrop-filter:blur(2px)]" setState(false);
: "pointer-events-none touch-none" }}
}`}
> >
<div <div
className={`fixed bg-shade inset-0 transition-all duration-500 ${ className={`fixed inset-0 z-50 grid place-content-center
props.state ? "bg-opacity-50" : "bg-opacity-0" transition-[backdrop-filter] duration-500 ${
}`} state ? "[backdrop-filter:blur(2px)]" : "pointer-events-none touch-none"
onClick={() => { }`}
props.setState(false);
}}
/>
<div
className={`p-10 grid gap-4 place-items-center transition-transform ${
props.state ? "scale-100" : "scale-0"
} ${
props.fillViewport
? "absolute inset-10 top-20"
: "relative max-h-[80vh] overflow-y-auto mobile:w-[85vw]"
} ${
props.hideBackground
? ""
: "bg-light rounded-lg shadow-2xl shadow-shade"
}`}
> >
{props.children} <div
className={`fixed bg-shade inset-0 transition-all duration-500 ${
state ? "bg-opacity-50" : "bg-opacity-0"
}`}
onClick={() => {
setState(false);
}}
/>
<div
className={`${
padding && "p-10 mobile:p-6"
} grid gap-4 place-items-center transition-transform ${
state ? "scale-100" : "scale-0"
} ${
fillViewport
? "absolute inset-10"
: "relative max-h-[80vh] overflow-y-auto mobile:w-[85vw]"
} ${
hideBackground ? "" : "bg-light rounded-lg shadow-2xl shadow-shade"
}`}
>
{children}
</div>
</div> </div>
</div> </Hotkeys>
); );
} }

View File

@ -1,29 +1,22 @@
import { GetPostQuery } from "graphql/generated"; import { AppStaticProps } from "graphql/getAppStaticProps";
import useSmartLanguage from "hooks/useSmartLanguage"; import { prettySlug } from "helpers/formatters";
import { AppStaticProps } from "queries/getAppStaticProps"; import { getStatusDescription } from "helpers/others";
import { getStatusDescription, prettySlug } from "queries/helpers"; import { Immutable, PostWithTranslations } from "helpers/types";
import AppLayout from "./AppLayout"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import Chip from "./Chip"; import { AppLayout } from "./AppLayout";
import HorizontalLine from "./HorizontalLine"; import { Chip } from "./Chip";
import Markdawn from "./Markdown/Markdawn"; import { HorizontalLine } from "./HorizontalLine";
import TOC from "./Markdown/TOC"; import { Markdawn } from "./Markdown/Markdawn";
import ReturnButton, { ReturnButtonType } from "./PanelComponents/ReturnButton"; import { TOC } from "./Markdown/TOC";
import ContentPanel from "./Panels/ContentPanel"; import { ReturnButton, ReturnButtonType } from "./PanelComponents/ReturnButton";
import SubPanel from "./Panels/SubPanel"; import { ContentPanel } from "./Panels/ContentPanel";
import RecorderChip from "./RecorderChip"; import { SubPanel } from "./Panels/SubPanel";
import ThumbnailHeader from "./ThumbnailHeader"; import { RecorderChip } from "./RecorderChip";
import ToolTip from "./ToolTip"; import { ThumbnailHeader } from "./ThumbnailHeader";
import { ToolTip } from "./ToolTip";
export type Post = Exclude<
Exclude<
GetPostQuery["posts"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>;
interface Props { interface Props {
post: Post; post: PostWithTranslations;
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
currencies: AppStaticProps["currencies"]; currencies: AppStaticProps["currencies"];
@ -38,7 +31,7 @@ interface Props {
appendBody?: JSX.Element; appendBody?: JSX.Element;
} }
export default function PostPage(props: Props): JSX.Element { export function PostPage(props: Immutable<Props>): JSX.Element {
const { const {
post, post,
langui, langui,

View File

@ -4,16 +4,18 @@ import {
PricePickerFragment, PricePickerFragment,
UploadImageFragment, UploadImageFragment,
} from "graphql/generated"; } from "graphql/generated";
import Link from "next/link"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { AppStaticProps } from "queries/getAppStaticProps";
import { import {
prettyDate, prettyDate,
prettyDuration, prettyDuration,
prettyPrice, prettyPrice,
prettyShortenNumber, prettyShortenNumber,
} from "queries/helpers"; } from "helpers/formatters";
import Chip from "./Chip"; import { ImageQuality } from "helpers/img";
import Img, { ImageQuality } from "./Img"; import { Immutable } from "helpers/types";
import Link from "next/link";
import { Chip } from "./Chip";
import { Img } from "./Img";
interface Props { interface Props {
thumbnail?: UploadImageFragment | string | null | undefined; thumbnail?: UploadImageFragment | string | null | undefined;
@ -26,6 +28,7 @@ interface Props {
topChips?: string[]; topChips?: string[];
bottomChips?: string[]; bottomChips?: string[];
keepInfoVisible?: boolean; keepInfoVisible?: boolean;
stackNumber?: number;
metadata?: { metadata?: {
currencies?: AppStaticProps["currencies"]; currencies?: AppStaticProps["currencies"];
release_date?: DatePickerFragment | null; release_date?: DatePickerFragment | null;
@ -42,7 +45,7 @@ interface Props {
| { __typename: "anotherHoverlayName" }; | { __typename: "anotherHoverlayName" };
} }
export default function ThumbnailPreview(props: Props): JSX.Element { export function PreviewCard(props: Immutable<Props>): JSX.Element {
const { const {
href, href,
thumbnail, thumbnail,
@ -50,6 +53,7 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
title, title,
subtitle, subtitle,
description, description,
stackNumber = 0,
topChips, topChips,
bottomChips, bottomChips,
keepInfoVisible, keepInfoVisible,
@ -110,8 +114,41 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
className="drop-shadow-shade-xl cursor-pointer grid items-end className="drop-shadow-shade-xl cursor-pointer grid items-end
fine:[--cover-opacity:0] hover:[--cover-opacity:1] hover:scale-[1.02] fine:[--cover-opacity:0] hover:[--cover-opacity:1] hover:scale-[1.02]
[--bg-opacity:0] hover:[--bg-opacity:0.5] [--play-opacity:0] [--bg-opacity:0] hover:[--bg-opacity:0.5] [--play-opacity:0]
hover:[--play-opacity:100] transition-transform" hover:[--play-opacity:100] transition-transform
[--stacked-top:0] hover:[--stacked-top:1]"
> >
{stackNumber > 0 && (
<>
<div
className="bg-light rounded-md overflow-hidden absolute transition-[top_transform]
inset-0 -top-[var(--stacked-top)*2.1rem] brightness-[0.8] sepia-[0.5]
scale-[calc(1-0.15*var(--stacked-top))]"
>
{thumbnail && (
<Img
className="opacity-30 "
image={thumbnail}
quality={ImageQuality.Medium}
/>
)}
</div>
<div
className="bg-light rounded-md overflow-hidden absolute transition-[top_transform]
-top-[var(--stacked-top)*1rem] inset-0 brightness-[0.9] sepia-[0.2]
scale-[calc(1-0.06*var(--stacked-top))]"
>
{thumbnail && (
<Img
className="opacity-70"
image={thumbnail}
quality={ImageQuality.Medium}
/>
)}
</div>
</>
)}
{thumbnail ? ( {thumbnail ? (
<div className="relative"> <div className="relative">
<Img <Img
@ -123,6 +160,14 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
image={thumbnail} image={thumbnail}
quality={ImageQuality.Medium} quality={ImageQuality.Medium}
/> />
{stackNumber > 0 && (
<div
className="absolute right-2 top-2 text-light bg-black
bg-opacity-60 px-2 rounded-full"
>
{stackNumber}
</div>
)}
{hoverlay && hoverlay.__typename === "Video" && ( {hoverlay && hoverlay.__typename === "Video" && (
<> <>
<div <div
@ -149,18 +194,26 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
) : ( ) : (
<div <div
style={{ aspectRatio: thumbnailAspectRatio }} style={{ aspectRatio: thumbnailAspectRatio }}
className={`w-full bg-light ${ className={`w-full bg-light relative ${
keepInfoVisible keepInfoVisible
? "rounded-t-md" ? "rounded-t-md"
: "rounded-md coarse:rounded-b-none" : "rounded-md coarse:rounded-b-none"
}`} }`}
></div> >
{stackNumber > 0 && (
<div
className="absolute right-2 top-2 text-light bg-black
bg-opacity-60 px-2 rounded-full"
>
{stackNumber}
</div>
)}
</div>
)} )}
<div <div
className={`linearbg-obi ${ className={`linearbg-obi ${
keepInfoVisible !keepInfoVisible &&
? "-mt-[0.3333em]" `fine:drop-shadow-shade-lg fine:absolute coarse:rounded-b-md
: `fine:drop-shadow-shade-lg fine:absolute coarse:rounded-b-md
bottom-2 -inset-x-0.5 opacity-[var(--cover-opacity)]` bottom-2 -inset-x-0.5 opacity-[var(--cover-opacity)]`
} transition-opacity z-20 grid p-4 gap-2`} } transition-opacity z-20 grid p-4 gap-2`}
> >
@ -173,11 +226,15 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
</div> </div>
)} )}
<div className="my-1"> <div className="my-1">
{pre_title && <p className="leading-none mb-1">{pre_title}</p>} {pre_title && (
{title && ( <p className="leading-none mb-1 break-words">{pre_title}</p>
<p className="font-headers text-lg leading-none">{title}</p>
)} )}
{subtitle && <p className="leading-none">{subtitle}</p>} {title && (
<p className="font-headers text-lg leading-none break-words">
{title}
</p>
)}
{subtitle && <p className="leading-none break-words">{subtitle}</p>}
</div> </div>
{description && <p>{description}</p>} {description && <p>{description}</p>}
{bottomChips && bottomChips.length > 0 && ( {bottomChips && bottomChips.length > 0 && (

View File

@ -1,7 +1,9 @@
import { UploadImageFragment } from "graphql/generated"; import { UploadImageFragment } from "graphql/generated";
import { ImageQuality } from "helpers/img";
import { Immutable } from "helpers/types";
import Link from "next/link"; import Link from "next/link";
import Chip from "./Chip"; import { Chip } from "./Chip";
import Img, { ImageQuality } from "./Img"; import { Img } from "./Img";
interface Props { interface Props {
thumbnail?: UploadImageFragment | string | null | undefined; thumbnail?: UploadImageFragment | string | null | undefined;
@ -14,7 +16,7 @@ interface Props {
bottomChips?: string[]; bottomChips?: string[];
} }
export default function PreviewLine(props: Props): JSX.Element { export function PreviewLine(props: Immutable<Props>): JSX.Element {
const { const {
href, href,
thumbnail, thumbnail,
@ -29,8 +31,9 @@ export default function PreviewLine(props: Props): JSX.Element {
return ( return (
<Link href={href} passHref> <Link href={href} passHref>
<div <div
className="drop-shadow-shade-xl rounded-md bg-light cursor-pointer hover:scale-[1.02] className="drop-shadow-shade-xl rounded-md bg-light cursor-pointer
transition-transform flex flex-row gap-4 overflow-hidden place-items-center pr-4 w-full h-36" hover:scale-[1.02] transition-transform flex flex-row gap-4
overflow-hidden place-items-center pr-4 w-full h-36"
> >
{thumbnail ? ( {thumbnail ? (
<div className="h-full aspect-[3/2]"> <div className="h-full aspect-[3/2]">

View File

@ -1,9 +1,11 @@
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import { RecorderChipFragment } from "graphql/generated"; import { RecorderChipFragment } from "graphql/generated";
import { AppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import Img, { ImageQuality } from "./Img"; import { ImageQuality } from "helpers/img";
import Markdawn from "./Markdown/Markdawn"; import { Immutable } from "helpers/types";
import ToolTip from "./ToolTip"; import { Img } from "./Img";
import { Markdawn } from "./Markdown/Markdawn";
import { ToolTip } from "./ToolTip";
interface Props { interface Props {
className?: string; className?: string;
@ -11,7 +13,7 @@ interface Props {
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
} }
export default function RecorderChip(props: Props): JSX.Element { export function RecorderChip(props: Immutable<Props>): JSX.Element {
const { recorder, langui } = props; const { recorder, langui } = props;
return ( return (
<ToolTip <ToolTip
@ -49,10 +51,7 @@ export default function RecorderChip(props: Props): JSX.Element {
)} )}
</div> </div>
</div> </div>
{recorder.bio?.[0] && <Markdawn text={recorder.bio[0].bio ?? ""} />} {recorder.bio?.[0] && <Markdawn text={recorder.bio[0].bio ?? ""} />}
{/* <Button className="cursor-not-allowed">View profile</Button> */}
</div> </div>
} }
placement="top" placement="top"

View File

@ -1,3 +1,4 @@
import { Immutable } from "helpers/types";
import Image from "next/image"; import Image from "next/image";
interface Props { interface Props {
@ -6,7 +7,7 @@ interface Props {
className?: string; className?: string;
} }
export default function SVG(props: Props): JSX.Element { export function SVG(props: Immutable<Props>): JSX.Element {
return ( return (
<div className={props.className}> <div className={props.className}>
<Image <Image

View File

@ -1,36 +1,31 @@
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Img, { ImageQuality } from "components/Img"; import { Img } from "components/Img";
import InsetBox from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import Markdawn from "components/Markdown/Markdawn"; import { Markdawn } from "components/Markdown/Markdawn";
import { GetContentQuery, UploadImageFragment } from "graphql/generated"; import { GetContentTextQuery, UploadImageFragment } from "graphql/generated";
import { AppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyinlineTitle, prettySlug, slugify } from "queries/helpers"; import { prettyinlineTitle, prettySlug, slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img";
import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox";
interface Props { interface Props {
pre_title?: string | null | undefined; pre_title?: string | null | undefined;
title: string | null | undefined; title: string | null | undefined;
subtitle?: string | null | undefined; subtitle?: string | null | undefined;
description?: string | null | undefined; description?: string | null | undefined;
type?: Exclude< type?: NonNullable<
Exclude< NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
GetContentQuery["contents"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>["type"]; >["type"];
categories?: Exclude< categories?: NonNullable<
Exclude< NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
GetContentQuery["contents"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>["categories"]; >["categories"];
thumbnail?: UploadImageFragment | null | undefined; thumbnail?: UploadImageFragment | null | undefined;
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
languageSwitcher?: JSX.Element; languageSwitcher?: JSX.Element;
} }
export default function ThumbnailHeader(props: Props): JSX.Element { export function ThumbnailHeader(props: Immutable<Props>): JSX.Element {
const { const {
langui, langui,
pre_title, pre_title,
@ -43,16 +38,21 @@ export default function ThumbnailHeader(props: Props): JSX.Element {
languageSwitcher, languageSwitcher,
} = props; } = props;
const [openLightBox, LightBox] = useLightBox();
return ( return (
<> <>
<LightBox />
<div className="grid place-items-center gap-12 mb-12"> <div className="grid place-items-center gap-12 mb-12">
<div className="drop-shadow-shade-lg"> <div className="drop-shadow-shade-lg">
{thumbnail ? ( {thumbnail ? (
<Img <Img
className=" rounded-xl" className="rounded-xl cursor-pointer"
image={thumbnail} image={thumbnail}
quality={ImageQuality.Medium} quality={ImageQuality.Medium}
priority onClick={() => {
openLightBox([getAssetURL(thumbnail.url, ImageQuality.Large)]);
}}
/> />
) : ( ) : (
<div className="w-96 aspect-[4/3] bg-light rounded-xl"></div> <div className="w-96 aspect-[4/3] bg-light rounded-xl"></div>

View File

@ -3,13 +3,13 @@ import "tippy.js/animations/scale-subtle.css";
interface Props extends TippyProps {} interface Props extends TippyProps {}
export default function ToolTip(props: Props): JSX.Element { export function ToolTip(props: Props): JSX.Element {
const newProps = { ...props }; const newProps: Props = {
delay: [150, 0],
// Set defaults interactive: true,
if (newProps.delay === undefined) newProps.delay = [150, 0]; animation: "scale-subtle",
if (newProps.interactive === undefined) newProps.interactive = true; ...props,
if (newProps.animation === undefined) newProps.animation = "scale-subtle"; };
return ( return (
<Tippy className={`text-[80%] ${newProps.className}`} {...newProps}> <Tippy className={`text-[80%] ${newProps.className}`} {...newProps}>

View File

@ -1,22 +1,20 @@
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import ToolTip from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { import {
Enum_Componenttranslationschronologyitem_Status, Enum_Componenttranslationschronologyitem_Status,
GetChronologyItemsQuery, GetChronologyItemsQuery,
} from "graphql/generated"; } from "graphql/generated";
import { AppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { getStatusDescription } from "queries/helpers"; import { getStatusDescription } from "helpers/others";
import { Immutable } from "helpers/types";
interface Props { interface Props {
item: Exclude< item: NonNullable<GetChronologyItemsQuery["chronologyItems"]>["data"][number];
GetChronologyItemsQuery["chronologyItems"],
null | undefined
>["data"][number];
displayYear: boolean; displayYear: boolean;
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
} }
export default function ChronologyItemComponent(props: Props): JSX.Element { export function ChronologyItemComponent(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
function generateAnchor( function generateAnchor(
@ -71,7 +69,8 @@ export default function ChronologyItemComponent(props: Props): JSX.Element {
if (props.item.attributes) { if (props.item.attributes) {
return ( return (
<div <div
className="grid place-content-start grid-rows-[auto_1fr] grid-cols-[4em] py-4 px-8 rounded-2xl target:bg-mid target:py-8 target:my-4" className="grid place-content-start grid-rows-[auto_1fr] grid-cols-[4em]
py-4 px-8 rounded-2xl target:bg-mid target:py-8 target:my-4"
id={generateAnchor( id={generateAnchor(
props.item.attributes.year, props.item.attributes.year,
props.item.attributes.month, props.item.attributes.month,
@ -100,7 +99,10 @@ export default function ChronologyItemComponent(props: Props): JSX.Element {
<> <>
{translation && ( {translation && (
<> <>
<div className="place-items-start place-content-start grid grid-flow-col gap-2"> <div
className="place-items-start
place-content-start grid grid-flow-col gap-2"
>
{translation.status !== {translation.status !==
Enum_Componenttranslationschronologyitem_Status.Done && ( Enum_Componenttranslationschronologyitem_Status.Done && (
<ToolTip <ToolTip
@ -125,7 +127,8 @@ export default function ChronologyItemComponent(props: Props): JSX.Element {
className={ className={
event.translations && event.translations &&
event.translations.length > 1 event.translations.length > 1
? "before:content-['-'] before:text-dark before:inline-block before:w-4 before:ml-[-1em] mt-2 whitespace-pre-line" ? `before:content-['-'] before:text-dark before:inline-block
before:w-4 before:ml-[-1em] mt-2 whitespace-pre-line`
: "whitespace-pre-line" : "whitespace-pre-line"
} }
> >

View File

@ -1,17 +1,17 @@
import ChronologyItemComponent from "components/Wiki/Chronology/ChronologyItemComponent"; import { ChronologyItemComponent } from "components/Wiki/Chronology/ChronologyItemComponent";
import { GetChronologyItemsQuery } from "graphql/generated"; import { GetChronologyItemsQuery } from "graphql/generated";
import { AppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
interface Props { interface Props {
year: number; year: number;
items: Exclude< items: NonNullable<
GetChronologyItemsQuery["chronologyItems"], GetChronologyItemsQuery["chronologyItems"]
null | undefined
>["data"][number][]; >["data"][number][];
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
} }
export default function ChronologyYearComponent(props: Props): JSX.Element { export function ChronologyYearComponent(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
return ( return (

View File

@ -1,6 +1,7 @@
import useDarkMode from "hooks/useDarkMode"; import { Immutable } from "helpers/types";
import useStateWithLocalStorage from "hooks/useStateWithLocalStorage"; import { useDarkMode } from "hooks/useDarkMode";
import React, { ReactNode, useContext } from "react"; import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage";
import React, { ReactNode, useContext, useState } from "react";
interface AppLayoutState { interface AppLayoutState {
subPanelOpen: boolean | undefined; subPanelOpen: boolean | undefined;
@ -14,6 +15,7 @@ interface AppLayoutState {
currency: string | undefined; currency: string | undefined;
playerName: string | undefined; playerName: string | undefined;
preferredLanguages: string[] | undefined; preferredLanguages: string[] | undefined;
menuGestures: boolean;
setSubPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>; setSubPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>; setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setMainPanelReduced: React.Dispatch< setMainPanelReduced: React.Dispatch<
@ -31,6 +33,7 @@ interface AppLayoutState {
setPreferredLanguages: React.Dispatch< setPreferredLanguages: React.Dispatch<
React.SetStateAction<string[] | undefined> React.SetStateAction<string[] | undefined>
>; >;
setMenuGestures: React.Dispatch<React.SetStateAction<boolean>>;
} }
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
@ -46,6 +49,7 @@ const initialState: AppLayoutState = {
currency: "USD", currency: "USD",
playerName: "", playerName: "",
preferredLanguages: [], preferredLanguages: [],
menuGestures: true,
setSubPanelOpen: () => {}, setSubPanelOpen: () => {},
setMainPanelReduced: () => {}, setMainPanelReduced: () => {},
setMainPanelOpen: () => {}, setMainPanelOpen: () => {},
@ -57,6 +61,7 @@ const initialState: AppLayoutState = {
setCurrency: () => {}, setCurrency: () => {},
setPlayerName: () => {}, setPlayerName: () => {},
setPreferredLanguages: () => {}, setPreferredLanguages: () => {},
setMenuGestures: () => {},
}; };
/* eslint-enable @typescript-eslint/no-empty-function */ /* eslint-enable @typescript-eslint/no-empty-function */
@ -72,7 +77,7 @@ interface Props {
children: ReactNode; children: ReactNode;
} }
export function AppContextProvider(props: Props): JSX.Element { export function AppContextProvider(props: Immutable<Props>): JSX.Element {
const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage< const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage<
boolean | undefined boolean | undefined
>("subPanelOpen", initialState.subPanelOpen); >("subPanelOpen", initialState.subPanelOpen);
@ -115,6 +120,8 @@ export function AppContextProvider(props: Props): JSX.Element {
string[] | undefined string[] | undefined
>("preferredLanguages", initialState.preferredLanguages); >("preferredLanguages", initialState.preferredLanguages);
const [menuGestures, setMenuGestures] = useState(false);
return ( return (
<AppContext.Provider <AppContext.Provider
value={{ value={{
@ -129,6 +136,7 @@ export function AppContextProvider(props: Props): JSX.Element {
currency, currency,
playerName, playerName,
preferredLanguages, preferredLanguages,
menuGestures,
setSubPanelOpen, setSubPanelOpen,
setConfigPanelOpen, setConfigPanelOpen,
setMainPanelReduced, setMainPanelReduced,
@ -140,6 +148,7 @@ export function AppContextProvider(props: Props): JSX.Element {
setCurrency, setCurrency,
setPlayerName, setPlayerName,
setPreferredLanguages, setPreferredLanguages,
setMenuGestures,
}} }}
> >
{props.children} {props.children}

View File

@ -4,22 +4,18 @@ import {
GetWebsiteInterfaceQuery, GetWebsiteInterfaceQuery,
} from "graphql/generated"; } from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
export interface AppStaticProps { export type AppStaticProps = Immutable<{
langui: Exclude< langui: NonNullable<
Exclude< NonNullable<
GetWebsiteInterfaceQuery["websiteInterfaces"], GetWebsiteInterfaceQuery["websiteInterfaces"]
null | undefined >["data"][number]["attributes"]
>["data"][number]["attributes"],
null | undefined
>; >;
currencies: Exclude< currencies: NonNullable<GetCurrenciesQuery["currencies"]>["data"];
GetCurrenciesQuery["currencies"], languages: NonNullable<GetLanguagesQuery["languages"]>["data"];
null | undefined }>;
>["data"];
languages: Exclude<GetLanguagesQuery["languages"], null | undefined>["data"];
}
export async function getAppStaticProps( export async function getAppStaticProps(
context: GetStaticPropsContext context: GetStaticPropsContext
@ -50,9 +46,11 @@ export async function getAppStaticProps(
}) })
).websiteInterfaces?.data[0].attributes; ).websiteInterfaces?.data[0].attributes;
return { const appStaticProps: AppStaticProps = {
langui: langui ?? ({} as AppStaticProps["langui"]), langui: langui ?? {},
currencies: currencies?.data ?? ({} as AppStaticProps["currencies"]), currencies: currencies?.data ?? [],
languages: languages?.data ?? ({} as AppStaticProps["languages"]), languages: languages?.data ?? [],
}; };
return appStaticProps;
} }

View File

@ -0,0 +1,32 @@
import { PostWithTranslations } from "helpers/types";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "./getAppStaticProps";
import { getReadySdk } from "./sdk";
export interface PostStaticProps extends AppStaticProps {
post: PostWithTranslations;
}
export function getPostStaticProps(
slug: string
): (
context: GetStaticPropsContext
) => Promise<{ notFound: boolean } | { props: PostStaticProps }> {
return async (context: GetStaticPropsContext) => {
const sdk = getReadySdk();
const post = await sdk.getPost({
slug: slug,
language_code: context.locale ?? "en",
});
if (post.posts?.data[0].attributes?.translations) {
const props: PostStaticProps = {
...(await getAppStaticProps(context)),
post: post.posts.data[0].attributes as PostWithTranslations,
};
return {
props: props,
};
}
return { notFound: true };
};
}

View File

@ -14,7 +14,13 @@ query devGetContents {
id id
} }
} }
titles {
ranged_contents {
data {
id
}
}
translations {
language { language {
data { data {
id id
@ -22,62 +28,36 @@ query devGetContents {
} }
title title
description description
} text_set {
ranged_contents { source_language {
data { data {
id id
} }
}
text_set {
language {
data {
id
} }
} status
source_language { transcribers {
data { data {
id id
}
} }
} translators {
status data {
transcribers { id
data { }
id
} }
} proofreaders {
translators { data {
data { id
id }
} }
text
} }
proofreaders {
data {
id
}
}
text
}
video_set {
id
}
audio_set {
id
} }
thumbnail { thumbnail {
data { data {
id id
} }
} }
next_recommended {
data {
id
}
}
previous_recommended {
data {
id
}
}
} }
} }
} }

View File

@ -1,77 +0,0 @@
query getContent($slug: String, $language_code: String) {
contents(filters: { slug: { eq: $slug } }) {
data {
attributes {
slug
titles(filters: { language: { code: { eq: $language_code } } }) {
pre_title
title
subtitle
description
}
categories {
data {
id
attributes {
name
short
}
}
}
type {
data {
attributes {
slug
titles(filters: { language: { code: { eq: $language_code } } }) {
title
}
}
}
}
ranged_contents {
data {
id
attributes {
slug
scan_set {
id
}
library_item {
data {
attributes {
slug
title
subtitle
thumbnail {
data {
attributes {
...uploadImage
}
}
}
}
}
}
}
}
}
text_set {
id
}
video_set {
id
}
audio_set {
id
}
thumbnail {
data {
attributes {
...uploadImage
}
}
}
}
}
}
}

View File

@ -4,12 +4,7 @@ query getContentText($slug: String, $language_code: String) {
id id
attributes { attributes {
slug slug
titles {
pre_title
title
subtitle
description
}
categories { categories {
data { data {
id id
@ -56,9 +51,7 @@ query getContentText($slug: String, $language_code: String) {
} }
} }
} }
text_set { translations {
status
text
language { language {
data { data {
attributes { attributes {
@ -66,39 +59,48 @@ query getContentText($slug: String, $language_code: String) {
} }
} }
} }
source_language { pre_title
data { title
attributes { subtitle
code description
text_set {
status
text
source_language {
data {
attributes {
code
}
} }
} }
} transcribers {
transcribers { data {
data { id
id attributes {
attributes { ...recorderChip
...recorderChip }
} }
} }
} translators {
translators { data {
data { id
id attributes {
attributes { ...recorderChip
...recorderChip }
} }
} }
} proofreaders {
proofreaders { data {
data { id
id attributes {
attributes { ...recorderChip
...recorderChip }
} }
} }
notes
} }
notes
} }
thumbnail { thumbnail {
data { data {
attributes { attributes {
@ -106,78 +108,47 @@ query getContentText($slug: String, $language_code: String) {
} }
} }
} }
previous_recommended { group {
data { data {
attributes { attributes {
slug contents {
titles(filters: { language: { code: { eq: $language_code } } }) {
pre_title
title
subtitle
}
categories {
data {
id
attributes {
short
}
}
}
type {
data { data {
attributes { attributes {
slug slug
titles( translations {
filters: { language: { code: { eq: $language_code } } } pre_title
) {
title title
subtitle
} }
} categories {
} data {
} id
thumbnail { attributes {
data { short
attributes { }
...uploadImage }
} }
} type {
} data {
} attributes {
} slug
} titles(
next_recommended { filters: {
data { language: { code: { eq: $language_code } }
attributes { }
slug ) {
titles(filters: { language: { code: { eq: $language_code } } }) { title
pre_title }
title }
subtitle }
} }
categories { thumbnail {
data { data {
id attributes {
attributes { ...uploadImage
short }
} }
}
}
type {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
} }
}
}
}
thumbnail {
data {
attributes {
...uploadImage
} }
} }
} }

View File

@ -4,7 +4,7 @@ query getContents($language_code: String) {
id id
attributes { attributes {
slug slug
titles(filters: { language: { code: { eq: $language_code } } }) { translations {
pre_title pre_title
title title
subtitle subtitle
@ -55,14 +55,17 @@ query getContents($language_code: String) {
} }
} }
} }
text_set { group {
id data {
} attributes {
video_set { combine
id contents {
} data {
audio_set { id
id }
}
}
}
} }
thumbnail { thumbnail {
data { data {

View File

@ -362,22 +362,18 @@ query getLibraryItem($slug: String, $language_code: String) {
} }
} }
} }
titles( translations {
filters: { language: { code: { eq: $language_code } } } language {
) { data {
attributes {
code
}
}
}
pre_title pre_title
title title
subtitle subtitle
} }
text_set {
id
}
video_set {
id
}
audio_set {
id
}
} }
} }
} }

View File

@ -132,6 +132,19 @@ query getWebsiteInterface($language_code: String) {
response_invalid_code response_invalid_code
response_invalid_email response_invalid_email
response_email_success response_email_success
always_show_info
item_not_available
primary_language
secondary_language
combine_related_contents
previous_content
followup_content
videos
view_on
channel
subscribers
description
available_at
} }
} }
} }

31
src/helpers/contents.ts Normal file
View File

@ -0,0 +1,31 @@
import { ContentWithTranslations, Immutable } from "./types";
type Group = Immutable<
NonNullable<
NonNullable<
NonNullable<
NonNullable<ContentWithTranslations["group"]>["data"]
>["attributes"]
>["contents"]
>["data"]
>;
export function getPreviousContent(group: Group, currentSlug: string) {
for (let index = 0; index < group.length; index += 1) {
const content = group[index];
if (content.attributes?.slug === currentSlug && index > 0) {
return group[index - 1];
}
}
return undefined;
}
export function getNextContent(group: Group, currentSlug: string) {
for (let index = 0; index < group.length; index += 1) {
const content = group[index];
if (content.attributes?.slug === currentSlug && index < group.length - 1) {
return group[index + 1];
}
}
return undefined;
}

View File

@ -1,20 +1,9 @@
import { import { DatePickerFragment, PricePickerFragment } from "graphql/generated";
getAssetURL, import { AppStaticProps } from "../graphql/getAppStaticProps";
getImgSizesByQuality, import { convertPrice } from "./numbers";
ImageQuality, import { Immutable } from "./types";
} from "components/Img";
import {
DatePickerFragment,
Enum_Componentsetstextset_Status,
GetCurrenciesQuery,
GetLibraryItemQuery,
GetLibraryItemScansQuery,
PricePickerFragment,
UploadImageFragment,
} from "graphql/generated";
import { AppStaticProps } from "./getAppStaticProps";
export function prettyDate(datePicker: DatePickerFragment): string { export function prettyDate(datePicker: Immutable<DatePickerFragment>): string {
let result = ""; let result = "";
if (datePicker.year) result += datePicker.year.toString(); if (datePicker.year) result += datePicker.year.toString();
if (datePicker.month) if (datePicker.month)
@ -25,7 +14,7 @@ export function prettyDate(datePicker: DatePickerFragment): string {
} }
export function prettyPrice( export function prettyPrice(
pricePicker: PricePickerFragment, pricePicker: Immutable<PricePickerFragment>,
currencies: AppStaticProps["currencies"], currencies: AppStaticProps["currencies"],
targetCurrencyCode?: string targetCurrencyCode?: string
): string { ): string {
@ -45,25 +34,6 @@ export function prettyPrice(
return result; return result;
} }
export function convertPrice(
pricePicker: PricePickerFragment,
targetCurrency: Exclude<
GetCurrenciesQuery["currencies"],
null | undefined
>["data"][number]
): number {
if (
pricePicker.amount &&
pricePicker.currency?.data?.attributes &&
targetCurrency.attributes
)
return (
(pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) /
targetCurrency.attributes.rate_to_usd
);
return 0;
}
export function prettySlug(slug?: string, parentSlug?: string): string { export function prettySlug(slug?: string, parentSlug?: string): string {
if (slug) { if (slug) {
if (parentSlug && slug.startsWith(parentSlug)) if (parentSlug && slug.startsWith(parentSlug))
@ -88,7 +58,7 @@ export function prettyinlineTitle(
} }
export function prettyItemType( export function prettyItemType(
metadata: any, metadata: Immutable<any>,
langui: AppStaticProps["langui"] langui: AppStaticProps["langui"]
): string | undefined | null { ): string | undefined | null {
switch (metadata.__typename) { switch (metadata.__typename) {
@ -110,7 +80,7 @@ export function prettyItemType(
} }
export function prettyItemSubType( export function prettyItemSubType(
metadata: metadata: Immutable<
| { | {
__typename: "ComponentMetadataAudio"; __typename: "ComponentMetadataAudio";
subtype?: { subtype?: {
@ -187,6 +157,7 @@ export function prettyItemSubType(
} }
| { __typename: "Error" } | { __typename: "Error" }
| null | null
>
): string { ): string {
if (metadata) { if (metadata) {
switch (metadata.__typename) { switch (metadata.__typename) {
@ -300,87 +271,6 @@ export function capitalizeString(string: string): string {
return words.join(" "); return words.join(" ");
} }
export function convertMmToInch(mm: number | null | undefined): string {
return mm ? (mm * 0.03937008).toPrecision(3) : "";
}
export interface OgImage {
image: string;
width: number;
height: number;
alt: string;
}
export function getOgImage(
quality: ImageQuality,
image: UploadImageFragment
): OgImage {
const imgSize = getImgSizesByQuality(
image.width ?? 0,
image.height ?? 0,
quality ? quality : ImageQuality.Small
);
return {
image: getAssetURL(image.url, quality),
width: imgSize.width,
height: imgSize.height,
alt: image.alternativeText || "",
};
}
export function sortContent(
contents:
| Exclude<
Exclude<
GetLibraryItemQuery["libraryItems"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>["contents"]
| Exclude<
Exclude<
GetLibraryItemScansQuery["libraryItems"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>["contents"]
) {
contents?.data.sort((a, b) => {
if (
a.attributes?.range[0]?.__typename === "ComponentRangePageRange" &&
b.attributes?.range[0]?.__typename === "ComponentRangePageRange"
) {
return (
a.attributes.range[0].starting_page -
b.attributes.range[0].starting_page
);
}
return 0;
});
}
export function getStatusDescription(
status: string,
langui: AppStaticProps["langui"]
): string | null | undefined {
switch (status) {
case Enum_Componentsetstextset_Status.Incomplete:
return langui.status_incomplete;
case Enum_Componentsetstextset_Status.Draft:
return langui.status_draft;
case Enum_Componentsetstextset_Status.Review:
return langui.status_review;
case Enum_Componentsetstextset_Status.Done:
return langui.status_done;
default:
return "";
}
}
export function slugify(string: string | undefined): string { export function slugify(string: string | undefined): string {
if (!string) { if (!string) {
return ""; return "";
@ -400,51 +290,3 @@ export function slugify(string: string | undefined): string {
.trim() .trim()
.replace(/ /gu, "-"); .replace(/ /gu, "-");
} }
export function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}
export function getLocalesFromLanguages(
languages?: Array<{
language?: {
data?: {
attributes?: { code: string } | null;
} | null;
} | null;
} | null> | null
) {
return languages
? languages.map((language) => language?.language?.data?.attributes?.code)
: [];
}
export function getVideoThumbnailURL(uid: string): string {
return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.webp`;
}
export function getVideoFile(uid: string): string {
return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.mp4`;
}
export function arrayMove<T>(arr: T[], old_index: number, new_index: number) {
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
return arr;
}
export function getPreferredLanguage(
preferredLanguages: (string | undefined)[],
availableLanguages: Map<string, number>
): number | undefined {
for (const locale of preferredLanguages) {
if (locale && availableLanguages.has(locale)) {
return availableLanguages.get(locale);
}
}
return undefined;
}
export function isInteger(value: string): boolean {
// eslint-disable-next-line require-unicode-regexp
return /^[+-]?[0-9]+$/.test(value);
}

88
src/helpers/img.ts Normal file
View File

@ -0,0 +1,88 @@
import { UploadImageFragment } from "graphql/generated";
import { Immutable } from "./types";
export enum ImageQuality {
Small = "small",
Medium = "medium",
Large = "large",
Og = "og",
}
export interface OgImage {
image: string;
width: number;
height: number;
alt: string;
}
export function getAssetFilename(path: string): string {
let result = path.split("/");
result = result[result.length - 1].split(".");
result = result
.splice(0, result.length - 1)
.join(".")
.split("_");
return result[0];
}
export function getAssetURL(
url: string,
quality: Immutable<ImageQuality>
): string {
let newUrl = url;
newUrl = newUrl.replace(/^\/uploads/u, `/${quality}`);
newUrl = newUrl.replace(/.jpg$/u, ".webp");
newUrl = newUrl.replace(/.jpeg$/u, ".webp");
newUrl = newUrl.replace(/.png$/u, ".webp");
if (quality === ImageQuality.Og) newUrl = newUrl.replace(/.webp$/u, ".jpg");
return process.env.NEXT_PUBLIC_URL_IMG + newUrl;
}
export function getImgSizesByMaxSize(
width: number,
height: number,
maxSize: number
): { width: number; height: number } {
if (width > height) {
if (width < maxSize) return { width: width, height: height };
return { width: maxSize, height: (height / width) * maxSize };
}
if (height < maxSize) return { width: width, height: height };
return { width: (width / height) * maxSize, height: maxSize };
}
export function getImgSizesByQuality(
width: number,
height: number,
quality: ImageQuality
): { width: number; height: number } {
switch (quality) {
case ImageQuality.Og:
return getImgSizesByMaxSize(width, height, 512);
case ImageQuality.Small:
return getImgSizesByMaxSize(width, height, 512);
case ImageQuality.Medium:
return getImgSizesByMaxSize(width, height, 1024);
case ImageQuality.Large:
return getImgSizesByMaxSize(width, height, 2048);
default:
return { width: 0, height: 0 };
}
}
export function getOgImage(
quality: Immutable<ImageQuality>,
image: Immutable<UploadImageFragment>
): OgImage {
const imgSize = getImgSizesByQuality(
image.width ?? 0,
image.height ?? 0,
quality ? quality : ImageQuality.Small
);
return {
image: getAssetURL(image.url, quality),
width: imgSize.width,
height: imgSize.height,
alt: image.alternativeText || "",
};
}

33
src/helpers/numbers.ts Normal file
View File

@ -0,0 +1,33 @@
import { GetCurrenciesQuery, PricePickerFragment } from "graphql/generated";
import { Immutable } from "./types";
export function convertPrice(
pricePicker: Immutable<PricePickerFragment>,
targetCurrency: Immutable<
NonNullable<GetCurrenciesQuery["currencies"]>["data"][number]
>
): number {
if (
pricePicker.amount &&
pricePicker.currency?.data?.attributes &&
targetCurrency.attributes
)
return (
(pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) /
targetCurrency.attributes.rate_to_usd
);
return 0;
}
export function convertMmToInch(mm: number | null | undefined): string {
return mm ? (mm * 0.03937008).toPrecision(3) : "";
}
export function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}
export function isInteger(value: string): boolean {
// eslint-disable-next-line require-unicode-regexp
return /^[+-]?[0-9]+$/.test(value);
}

66
src/helpers/others.ts Normal file
View File

@ -0,0 +1,66 @@
import {
Enum_Componentsetstextset_Status,
GetLibraryItemQuery,
GetLibraryItemScansQuery,
} from "graphql/generated";
import { AppStaticProps } from "../graphql/getAppStaticProps";
import { Immutable } from "./types";
type SortContentProps =
| NonNullable<
NonNullable<
GetLibraryItemQuery["libraryItems"]
>["data"][number]["attributes"]
>["contents"]
| NonNullable<
NonNullable<
GetLibraryItemScansQuery["libraryItems"]
>["data"][number]["attributes"]
>["contents"];
export function sortContent(contents: Immutable<SortContentProps>) {
if (contents) {
const newContent = { ...contents } as SortContentProps;
newContent?.data.sort((a, b) => {
if (
a.attributes?.range[0]?.__typename === "ComponentRangePageRange" &&
b.attributes?.range[0]?.__typename === "ComponentRangePageRange"
) {
return (
a.attributes.range[0].starting_page -
b.attributes.range[0].starting_page
);
}
return 0;
});
return newContent as Immutable<SortContentProps>;
}
return contents;
}
export function getStatusDescription(
status: string,
langui: AppStaticProps["langui"]
): string | null | undefined {
switch (status) {
case Enum_Componentsetstextset_Status.Incomplete:
return langui.status_incomplete;
case Enum_Componentsetstextset_Status.Draft:
return langui.status_draft;
case Enum_Componentsetstextset_Status.Review:
return langui.status_review;
case Enum_Componentsetstextset_Status.Done:
return langui.status_done;
default:
return "";
}
}
export function arrayMove<T>(arr: T[], old_index: number, new_index: number) {
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
return arr;
}

26
src/helpers/types.ts Normal file
View File

@ -0,0 +1,26 @@
import { GetContentTextQuery, GetPostQuery } from "graphql/generated";
import React from "react";
type Post = NonNullable<
NonNullable<GetPostQuery["posts"]>["data"][number]["attributes"]
>;
export interface PostWithTranslations extends Omit<Post, "translations"> {
translations: NonNullable<Post["translations"]>;
}
type Content = NonNullable<
NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
>;
export interface ContentWithTranslations extends Omit<Content, "translations"> {
translations: NonNullable<Content["translations"]>;
}
type ImmutableBlackList<T> = JSX.Element | React.ReactNode | Function;
export type Immutable<T> = {
readonly [K in keyof T]: T[K] extends ImmutableBlackList<T>
? T[K]
: Immutable<T[K]>;
};

7
src/helpers/videos.ts Normal file
View File

@ -0,0 +1,7 @@
export function getVideoThumbnailURL(uid: string): string {
return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.webp`;
}
export function getVideoFile(uid: string): string {
return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.mp4`;
}

View File

@ -1,8 +1,8 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { usePrefersDarkMode } from "./useMediaQuery"; import { usePrefersDarkMode } from "./useMediaQuery";
import useStateWithLocalStorage from "./useStateWithLocalStorage"; import { useStateWithLocalStorage } from "./useStateWithLocalStorage";
export default function useDarkMode( export function useDarkMode(
key: string, key: string,
initialValue: boolean | undefined initialValue: boolean | undefined
): [ ): [

28
src/hooks/useLightBox.tsx Normal file
View File

@ -0,0 +1,28 @@
import { LightBox } from "components/LightBox";
import { useState } from "react";
export function useLightBox(): [
(images: string[], index?: number) => void,
() => JSX.Element
] {
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxImages, setLightboxImages] = useState([""]);
const [lightboxIndex, setLightboxIndex] = useState(0);
return [
(images: string[], index = 0) => {
setLightboxOpen(true);
setLightboxImages(images);
setLightboxIndex(index);
},
() => (
<LightBox
state={lightboxOpen}
setState={setLightboxOpen}
images={lightboxImages}
index={lightboxIndex}
setIndex={setLightboxIndex}
/>
),
];
}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function useMediaQuery(query: string): boolean { export function useMediaQuery(query: string): boolean {
function getMatches(query: string): boolean { function getMatches(query: string): boolean {
// Prevents SSR issues // Prevents SSR issues
if (typeof window !== "undefined") { if (typeof window !== "undefined") {

View File

@ -1,20 +1,32 @@
import LanguageSwitcher from "components/Inputs/LanguageSwitcher"; import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AppStaticProps } from "queries/getAppStaticProps";
import { getPreferredLanguage } from "queries/helpers";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
interface Props<T> { interface Props<T> {
items: T[]; items: Immutable<T[]>;
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
languageExtractor: (item: T) => string | undefined; languageExtractor: (item: NonNullable<Immutable<T>>) => string | undefined;
transform?: (item: T) => T; transform?: (item: NonNullable<Immutable<T>>) => NonNullable<Immutable<T>>;
} }
export default function useSmartLanguage<T>( function getPreferredLanguage(
preferredLanguages: (string | undefined)[],
availableLanguages: Map<string, number>
): number | undefined {
for (const locale of preferredLanguages) {
if (locale && availableLanguages.has(locale)) {
return availableLanguages.get(locale);
}
}
return undefined;
}
export function useSmartLanguage<T>(
props: Props<T> props: Props<T>
): [T | undefined, () => JSX.Element] { ): [Immutable<T | undefined>, () => JSX.Element] {
const { const {
items, items,
languageExtractor, languageExtractor,
@ -28,12 +40,15 @@ export default function useSmartLanguage<T>(
const [selectedTranslationIndex, setSelectedTranslationIndex] = useState< const [selectedTranslationIndex, setSelectedTranslationIndex] = useState<
number | undefined number | undefined
>(); >();
const [selectedTranslation, setSelectedTranslation] = useState<T>(); const [selectedTranslation, setSelectedTranslation] =
useState<Immutable<T>>();
useEffect(() => { useEffect(() => {
items.map((elem, index) => { items.map((elem, index) => {
const result = languageExtractor(elem); if (elem !== null && elem !== undefined) {
if (result !== undefined) availableLocales.set(result, index); const result = languageExtractor(elem);
if (result !== undefined) availableLocales.set(result, index);
}
}); });
}, [availableLocales, items, languageExtractor]); }, [availableLocales, items, languageExtractor]);
@ -47,8 +62,9 @@ export default function useSmartLanguage<T>(
}, [appLayout.preferredLanguages, availableLocales, router.locale]); }, [appLayout.preferredLanguages, availableLocales, router.locale]);
useEffect(() => { useEffect(() => {
if (selectedTranslationIndex !== undefined) if (selectedTranslationIndex !== undefined) {
setSelectedTranslation(transform(items[selectedTranslationIndex])); setSelectedTranslation(transform(items[selectedTranslationIndex]));
}
}, [items, selectedTranslationIndex, transform]); }, [items, selectedTranslationIndex, transform]);
return [ return [

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function useStateWithLocalStorage<T>( export function useStateWithLocalStorage<T>(
key: string, key: string,
initialValue: T initialValue: T
): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>] { ): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>] {

View File

@ -1,14 +1,16 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import ReturnButton, { import {
ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel"; import { ContentPanel } from "components/Panels/ContentPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function FourOhFour(props: Props): JSX.Element { export default function FourOhFour(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const contentPanel = ( const contentPanel = (
<ContentPanel> <ContentPanel>

View File

@ -1,14 +1,16 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import ReturnButton, { import {
ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel"; import { ContentPanel } from "components/Panels/ContentPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function FiveHundred(props: Props): JSX.Element { export default function FiveHundred(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const contentPanel = ( const contentPanel = (
<ContentPanel> <ContentPanel>

View File

@ -6,7 +6,7 @@ import Document, {
NextScript, NextScript,
} from "next/document"; } from "next/document";
class MyDocument extends Document { export default class MyDocument extends Document {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static async getInitialProps(ctx: DocumentContext) { static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx);
@ -65,5 +65,3 @@ class MyDocument extends Document {
); );
} }
} }
export default MyDocument;

View File

@ -1,13 +1,13 @@
import PostPage, { Post } from "components/PostPage"; import { PostPage } from "components/PostPage";
import { getReadySdk } from "graphql/sdk"; import {
import { GetStaticPropsContext } from "next"; getPostStaticProps,
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; PostStaticProps,
} from "graphql/getPostStaticProps";
import { Immutable } from "helpers/types";
interface Props extends AppStaticProps { export default function AccordsHandbook(
post: Post; props: Immutable<PostStaticProps>
} ): JSX.Element {
export default function AccordsHandbook(props: Props): JSX.Element {
const { post, langui, languages, currencies } = props; const { post, langui, languages, currencies } = props;
return ( return (
<PostPage <PostPage
@ -23,21 +23,4 @@ export default function AccordsHandbook(props: Props): JSX.Element {
); );
} }
export async function getStaticProps( export const getStaticProps = getPostStaticProps("accords-handbook");
context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
const slug = "accords-handbook";
const post = await sdk.getPost({
slug: slug,
language_code: context.locale ?? "en",
});
if (!post.posts?.data[0].attributes) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
post: post.posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -1,18 +1,18 @@
import InsetBox from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import PostPage, { Post } from "components/PostPage"; import { PostPage } from "components/PostPage";
import { getReadySdk } from "graphql/sdk"; import {
import { GetStaticPropsContext } from "next"; getPostStaticProps,
PostStaticProps,
} from "graphql/getPostStaticProps";
import { randomInt } from "helpers/numbers";
import { Immutable } from "helpers/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { RequestMailProps, ResponseMailProps } from "pages/api/mail"; import { RequestMailProps, ResponseMailProps } from "pages/api/mail";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { randomInt } from "queries/helpers";
import { useState } from "react"; import { useState } from "react";
interface Props extends AppStaticProps { export default function AboutUs(
post: Post; props: Immutable<PostStaticProps>
} ): JSX.Element {
export default function AboutUs(props: Props): JSX.Element {
const { post, langui, languages, currencies } = props; const { post, langui, languages, currencies } = props;
const router = useRouter(); const router = useRouter();
@ -181,21 +181,4 @@ export default function AboutUs(props: Props): JSX.Element {
); );
} }
export async function getStaticProps( export const getStaticProps = getPostStaticProps("contact");
context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
const slug = "contact";
const post = await sdk.getPost({
slug: slug,
language_code: context.locale ?? "en",
});
if (!post.posts?.data[0].attributes) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
post: post.posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -1,13 +1,14 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import NavOption from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function AboutUs(props: Props): JSX.Element { export default function AboutUs(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>

View File

@ -1,13 +1,10 @@
import PostPage, { Post } from "components/PostPage"; import { PostPage } from "components/PostPage";
import { getReadySdk } from "graphql/sdk"; import {
import { GetStaticPropsContext } from "next"; getPostStaticProps,
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; PostStaticProps,
} from "graphql/getPostStaticProps";
interface Props extends AppStaticProps { export default function Legality(props: PostStaticProps): JSX.Element {
post: Post;
}
export default function SiteInformation(props: Props): JSX.Element {
const { post, langui, languages, currencies } = props; const { post, langui, languages, currencies } = props;
return ( return (
<PostPage <PostPage
@ -23,21 +20,4 @@ export default function SiteInformation(props: Props): JSX.Element {
); );
} }
export async function getStaticProps( export const getStaticProps = getPostStaticProps("legality");
context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
const slug = "legality";
const post = await sdk.getPost({
slug: slug,
language_code: context.locale ?? "en",
});
if (!post.posts?.data[0].attributes) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
post: post.posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -1,12 +1,10 @@
import PostPage, { Post } from "components/PostPage"; import { PostPage } from "components/PostPage";
import { getReadySdk } from "graphql/sdk"; import {
import { GetStaticPropsContext } from "next"; getPostStaticProps,
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; PostStaticProps,
} from "graphql/getPostStaticProps";
interface Props extends AppStaticProps { export default function SharingPolicy(props: PostStaticProps): JSX.Element {
post: Post;
}
export default function SharingPolicy(props: Props): JSX.Element {
const { post, langui, languages, currencies } = props; const { post, langui, languages, currencies } = props;
return ( return (
<PostPage <PostPage
@ -22,21 +20,4 @@ export default function SharingPolicy(props: Props): JSX.Element {
); );
} }
export async function getStaticProps( export const getStaticProps = getPostStaticProps("sharing-policy");
context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
const slug = "sharing-policy";
const post = await sdk.getPost({
slug: slug,
language_code: context.locale ?? "en",
});
if (!post.posts?.data[0].attributes) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
post: post.posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -14,7 +14,7 @@ export interface RequestMailProps {
formName: string; formName: string;
} }
export default async function Mail( export async function Mail(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<ResponseMailProps> res: NextApiResponse<ResponseMailProps>
) { ) {

View File

@ -70,7 +70,7 @@ type ResponseMailProps = {
revalidated: boolean; revalidated: boolean;
}; };
export default async function Mail( export async function Mail(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<ResponseMailProps> res: NextApiResponse<ResponseMailProps>
) { ) {

View File

@ -1,13 +1,14 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import NavOption from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Archives(props: Props): JSX.Element { export default function Archives(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>

View File

@ -1,29 +1,30 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Switch from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import ReturnButton, { import {
ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import ThumbnailPreview from "components/PreviewCard"; import { PreviewCard } from "components/PreviewCard";
import { GetVideoChannelQuery } from "graphql/generated"; import { GetVideoChannelQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { getVideoThumbnailURL } from "helpers/videos";
import { import {
GetStaticPathsContext, GetStaticPathsContext,
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getVideoThumbnailURL } from "queries/helpers";
import { useState } from "react"; import { useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
channel: Exclude< channel: NonNullable<
GetVideoChannelQuery["videoChannels"], GetVideoChannelQuery["videoChannels"]
null | undefined
>["data"][number]["attributes"]; >["data"][number]["attributes"];
} }
@ -35,7 +36,7 @@ export default function Channel(props: Props): JSX.Element {
<SubPanel> <SubPanel>
<ReturnButton <ReturnButton
href="/archives/videos/" href="/archives/videos/"
title={"Videos"} title={langui.videos}
langui={langui} langui={langui}
displayOn={ReturnButtonType.desktop} displayOn={ReturnButtonType.desktop}
className="mb-10" className="mb-10"
@ -43,12 +44,12 @@ export default function Channel(props: Props): JSX.Element {
<PanelHeader <PanelHeader
icon="movie" icon="movie"
title="Videos" title={langui.videos}
description={langui.archives_description} description={langui.archives_description}
/> />
<div className="flex flex-row gap-2 place-items-center coarse:hidden"> <div className="flex flex-row gap-2 place-items-center coarse:hidden">
<p className="flex-shrink-0">{"Always show info"}:</p> <p className="flex-shrink-0">{langui.always_show_info}:</p>
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
</div> </div>
</SubPanel> </SubPanel>
@ -60,11 +61,15 @@ export default function Channel(props: Props): JSX.Element {
<h1 className="text-3xl">{channel?.title}</h1> <h1 className="text-3xl">{channel?.title}</h1>
<p>{channel?.subscribers.toLocaleString()} subscribers</p> <p>{channel?.subscribers.toLocaleString()} subscribers</p>
</div> </div>
<div className="grid gap-8 items-start mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"> <div
className="grid gap-8 items-start mobile:grid-cols-2
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]
pb-12 border-b-[3px] border-dotted last-of-type:border-0"
>
{channel?.videos?.data.map((video) => ( {channel?.videos?.data.map((video) => (
<> <>
{video.attributes && ( {video.attributes && (
<ThumbnailPreview <PreviewCard
key={video.id} key={video.id}
href={`/archives/videos/v/${video.attributes.uid}`} href={`/archives/videos/v/${video.attributes.uid}`}
title={video.attributes.title} title={video.attributes.title}

View File

@ -1,24 +1,27 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import PageSelector from "components/Inputs/PageSelector"; import { PageSelector } from "components/Inputs/PageSelector";
import Switch from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import ReturnButton, { import {
ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import ThumbnailPreview from "components/PreviewCard"; import { PreviewCard } from "components/PreviewCard";
import { GetVideosPreviewQuery } from "graphql/generated"; import { GetVideosPreviewQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyDate } from "helpers/formatters";
import { getVideoThumbnailURL } from "helpers/videos";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getVideoThumbnailURL, prettyDate } from "queries/helpers";
import { useState } from "react"; import { useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
videos: Exclude<GetVideosPreviewQuery["videos"], null | undefined>["data"]; videos: NonNullable<GetVideosPreviewQuery["videos"]>["data"];
} }
export default function Videos(props: Props): JSX.Element { export default function Videos(props: Props): JSX.Element {
@ -64,7 +67,7 @@ export default function Videos(props: Props): JSX.Element {
/> />
<div className="flex flex-row gap-2 place-items-center coarse:hidden"> <div className="flex flex-row gap-2 place-items-center coarse:hidden">
<p className="flex-shrink-0">{"Always show info"}:</p> <p className="flex-shrink-0">{langui.always_show_info}:</p>
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
</div> </div>
</SubPanel> </SubPanel>
@ -79,11 +82,15 @@ export default function Videos(props: Props): JSX.Element {
className="mb-12" className="mb-12"
/> />
<div className="grid gap-8 items-start thin:grid-cols-1 mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"> <div
className="grid gap-8 items-start thin:grid-cols-1 mobile:grid-cols-2
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]
pb-12 border-b-[3px] border-dotted last-of-type:border-0"
>
{paginatedVideos[page].map((video) => ( {paginatedVideos[page].map((video) => (
<> <>
{video.attributes && ( {video.attributes && (
<ThumbnailPreview <PreviewCard
key={video.id} key={video.id}
href={`/archives/videos/v/${video.attributes.uid}`} href={`/archives/videos/v/${video.attributes.uid}`}
title={video.attributes.title} title={video.attributes.title}

View File

@ -1,34 +1,33 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import HorizontalLine from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import InsetBox from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import NavOption from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
import ReturnButton, { import {
ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { GetVideoQuery } from "graphql/generated"; import { GetVideoQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
import { getVideoFile } from "helpers/videos";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import { import {
GetStaticPathsContext, GetStaticPathsContext,
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getVideoFile, prettyDate, prettyShortenNumber } from "queries/helpers";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
video: Exclude< video: NonNullable<
Exclude< NonNullable<GetVideoQuery["videos"]>["data"][number]["attributes"]
GetVideoQuery["videos"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>; >;
} }
@ -40,7 +39,7 @@ export default function Video(props: Props): JSX.Element {
<SubPanel> <SubPanel>
<ReturnButton <ReturnButton
href="/archives/videos/" href="/archives/videos/"
title={"Videos"} title={langui.videos}
langui={langui} langui={langui}
displayOn={ReturnButtonType.desktop} displayOn={ReturnButtonType.desktop}
className="mb-10" className="mb-10"
@ -56,14 +55,14 @@ export default function Video(props: Props): JSX.Element {
/> />
<NavOption <NavOption
title={"Channel"} title={langui.channel}
url="#channel" url="#channel"
border border
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => appLayout.setSubPanelOpen(false)}
/> />
<NavOption <NavOption
title={"Description"} title={langui.description}
url="#description" url="#description"
border border
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => appLayout.setSubPanelOpen(false)}
@ -98,7 +97,8 @@ export default function Video(props: Props): JSX.Element {
className="w-full aspect-video" className="w-full aspect-video"
title="YouTube video player" title="YouTube video player"
frameBorder="0" frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write;
encrypted-media; gyroscope; picture-in-picture"
allowFullScreen allowFullScreen
></iframe> ></iframe>
)} )}
@ -135,7 +135,7 @@ export default function Video(props: Props): JSX.Element {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<Button className="!py-0 !px-3">{`View on ${video.source}`}</Button> <Button className="!py-0 !px-3">{`${langui.view_on} ${video.source}`}</Button>
</a> </a>
</div> </div>
</div> </div>
@ -144,7 +144,7 @@ export default function Video(props: Props): JSX.Element {
{video.channel?.data?.attributes && ( {video.channel?.data?.attributes && (
<InsetBox id="channel" className="grid place-items-center"> <InsetBox id="channel" className="grid place-items-center">
<div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-4 text-center"> <div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-4 text-center">
<h2 className="text-2xl">{"Channel"}</h2> <h2 className="text-2xl">{langui.channel}</h2>
<div> <div>
<Button <Button
href={`/archives/videos/c/${video.channel.data.attributes.uid}`} href={`/archives/videos/c/${video.channel.data.attributes.uid}`}
@ -153,8 +153,7 @@ export default function Video(props: Props): JSX.Element {
</Button> </Button>
<p> <p>
{video.channel.data.attributes.subscribers.toLocaleString()}{" "} {`${video.channel.data.attributes.subscribers.toLocaleString()} ${langui.subscribers?.toLowerCase()}`}
subscribers
</p> </p>
</div> </div>
</div> </div>
@ -163,7 +162,7 @@ export default function Video(props: Props): JSX.Element {
<InsetBox id="description" className="grid place-items-center"> <InsetBox id="description" className="grid place-items-center">
<div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-8"> <div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-8">
<h2 className="text-2xl">{"Description"}</h2> <h2 className="text-2xl">{langui.description}</h2>
<p className="whitespace-pre-line">{video.description}</p> <p className="whitespace-pre-line">{video.description}</p>
</div> </div>
</InsetBox> </InsetBox>

View File

@ -1,12 +1,13 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Chronicles(props: Props): JSX.Element { export default function Chronicles(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>

View File

@ -1,56 +1,60 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import HorizontalLine from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import Markdawn from "components/Markdown/Markdawn"; import { Markdawn } from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC"; import { TOC } from "components/Markdown/TOC";
import ReturnButton, { import {
ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel"; import { ContentPanel } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import PreviewLine from "components/PreviewLine"; import { PreviewLine } from "components/PreviewLine";
import RecorderChip from "components/RecorderChip"; import { RecorderChip } from "components/RecorderChip";
import ThumbnailHeader from "components/ThumbnailHeader"; import { ThumbnailHeader } from "components/ThumbnailHeader";
import ToolTip from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { GetContentTextQuery } from "graphql/generated"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { getNextContent, getPreviousContent } from "helpers/contents";
import {
prettyinlineTitle,
prettyLanguage,
prettySlug,
} from "helpers/formatters";
import { getStatusDescription } from "helpers/others";
import { ContentWithTranslations, Immutable } from "helpers/types";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import useSmartLanguage from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { import {
GetStaticPathsContext, GetStaticPathsContext,
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import {
getStatusDescription,
prettyinlineTitle,
prettyLanguage,
prettySlug,
} from "queries/helpers";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
content: Exclude< content: ContentWithTranslations;
GetContentTextQuery["contents"],
null | undefined
>["data"][number]["attributes"];
contentId: Exclude<
GetContentTextQuery["contents"],
null | undefined
>["data"][number]["id"];
} }
export default function Content(props: Props): JSX.Element { export default function Content(props: Immutable<Props>): JSX.Element {
const { langui, content, languages } = props; const { langui, content, languages } = props;
const isMobile = useMediaMobile(); const isMobile = useMediaMobile();
const [selectedTextSet, LanguageSwitcher] = useSmartLanguage({ const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({
items: content?.text_set, items: content.translations,
languages: languages, languages: languages,
languageExtractor: (item) => item?.language?.data?.attributes?.code, languageExtractor: (item) => item.language?.data?.attributes?.code,
}); });
const selectedTitle = content?.titles?.[0]; const previousContent = content.group?.data?.attributes?.contents
? getPreviousContent(
content.group.data.attributes.contents.data,
content.slug
)
: undefined;
const nextContent = content.group?.data?.attributes?.contents
? getNextContent(content.group.data.attributes.contents.data, content.slug)
: undefined;
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
@ -62,124 +66,133 @@ export default function Content(props: Props): JSX.Element {
horizontalLine horizontalLine
/> />
{selectedTextSet?.source_language?.data?.attributes && ( {selectedTranslation?.text_set && (
<div className="grid gap-5"> <div className="grid gap-5">
<h2 className="text-xl"> <h2 className="text-xl">
{selectedTextSet.source_language.data.attributes.code === {selectedTranslation.text_set.source_language?.data?.attributes
selectedTextSet.language?.data?.attributes?.code ?.code === selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice ? langui.transcript_notice
: langui.translation_notice} : langui.translation_notice}
</h2> </h2>
{selectedTextSet.source_language.data.attributes.code !== {selectedTranslation.text_set.source_language?.data?.attributes
selectedTextSet.language?.data?.attributes?.code && ( ?.code &&
<div className="grid place-items-center gap-2"> selectedTranslation.text_set.source_language.data.attributes
<p className="font-headers">{langui.source_language}:</p> .code !==
<Chip> selectedTranslation.language?.data?.attributes?.code && (
{prettyLanguage( <div className="grid place-items-center gap-2">
selectedTextSet.source_language.data.attributes.code, <p className="font-headers">{langui.source_language}:</p>
languages <Chip>
)} {prettyLanguage(
</Chip> selectedTranslation.text_set.source_language.data.attributes
</div> .code,
)} languages
)}
</Chip>
</div>
)}
<div className="grid grid-flow-col place-items-center place-content-center gap-2"> <div className="grid grid-flow-col place-items-center place-content-center gap-2">
<p className="font-headers">{langui.status}:</p> <p className="font-headers">{langui.status}:</p>
<ToolTip <ToolTip
content={getStatusDescription(selectedTextSet.status, langui)} content={getStatusDescription(
selectedTranslation.text_set.status,
langui
)}
maxWidth={"20rem"} maxWidth={"20rem"}
> >
<Chip>{selectedTextSet.status}</Chip> <Chip>{selectedTranslation.text_set.status}</Chip>
</ToolTip> </ToolTip>
</div> </div>
{selectedTextSet.transcribers && {selectedTranslation.text_set.transcribers &&
selectedTextSet.transcribers.data.length > 0 && ( selectedTranslation.text_set.transcribers.data.length > 0 && (
<div> <div>
<p className="font-headers">{langui.transcribers}:</p> <p className="font-headers">{langui.transcribers}:</p>
<div className="grid place-items-center place-content-center gap-2"> <div className="grid place-items-center place-content-center gap-2">
{selectedTextSet.transcribers.data.map((recorder) => ( {selectedTranslation.text_set.transcribers.data.map(
<> (recorder) => (
{recorder.attributes && ( <>
<RecorderChip {recorder.attributes && (
key={recorder.id} <RecorderChip
langui={langui} key={recorder.id}
recorder={recorder.attributes} langui={langui}
/> recorder={recorder.attributes}
)} />
</> )}
))} </>
)
)}
</div> </div>
</div> </div>
)} )}
{selectedTextSet.translators && {selectedTranslation.text_set.translators &&
selectedTextSet.translators.data.length > 0 && ( selectedTranslation.text_set.translators.data.length > 0 && (
<div> <div>
<p className="font-headers">{langui.translators}:</p> <p className="font-headers">{langui.translators}:</p>
<div className="grid place-items-center place-content-center gap-2"> <div className="grid place-items-center place-content-center gap-2">
{selectedTextSet.translators.data.map((recorder) => ( {selectedTranslation.text_set.translators.data.map(
<> (recorder) => (
{recorder.attributes && ( <>
<RecorderChip {recorder.attributes && (
key={recorder.id} <RecorderChip
langui={langui} key={recorder.id}
recorder={recorder.attributes} langui={langui}
/> recorder={recorder.attributes}
)} />
</> )}
))} </>
)
)}
</div> </div>
</div> </div>
)} )}
{selectedTextSet.proofreaders && {selectedTranslation.text_set.proofreaders &&
selectedTextSet.proofreaders.data.length > 0 && ( selectedTranslation.text_set.proofreaders.data.length > 0 && (
<div> <div>
<p className="font-headers">{langui.proofreaders}:</p> <p className="font-headers">{langui.proofreaders}:</p>
<div className="grid place-items-center place-content-center gap-2"> <div className="grid place-items-center place-content-center gap-2">
{selectedTextSet.proofreaders.data.map((recorder) => ( {selectedTranslation.text_set.proofreaders.data.map(
<> (recorder) => (
{recorder.attributes && ( <>
<RecorderChip {recorder.attributes && (
key={recorder.id} <RecorderChip
langui={langui} key={recorder.id}
recorder={recorder.attributes} langui={langui}
/> recorder={recorder.attributes}
)} />
</> )}
))} </>
)
)}
</div> </div>
</div> </div>
)} )}
{selectedTextSet.notes && ( {selectedTranslation.text_set.notes && (
<div> <div>
<p className="font-headers">{"Notes"}:</p> <p className="font-headers">{"Notes"}:</p>
<div className="grid place-items-center place-content-center gap-2"> <div className="grid place-items-center place-content-center gap-2">
<Markdawn text={selectedTextSet.notes} /> <Markdawn text={selectedTranslation.text_set.notes} />
</div> </div>
</div> </div>
)} )}
</div> </div>
)} )}
{selectedTextSet && content?.text_set && selectedTextSet.text && ( {selectedTranslation?.text_set?.text && (
<> <>
<HorizontalLine /> <HorizontalLine />
<TOC <TOC
text={selectedTextSet.text} text={selectedTranslation.text_set.text}
title={ title={prettyinlineTitle(
content.titles && content.titles.length > 0 && selectedTitle selectedTranslation.pre_title,
? prettyinlineTitle( selectedTranslation.title,
selectedTitle.pre_title, selectedTranslation.subtitle
selectedTitle.title, )}
selectedTitle.subtitle
)
: prettySlug(content.slug)
}
/> />
</> </>
)} )}
@ -188,142 +201,119 @@ export default function Content(props: Props): JSX.Element {
const contentPanel = ( const contentPanel = (
<ContentPanel> <ContentPanel>
<ReturnButton <ReturnButton
href={`/contents/${content?.slug}`} href={`/contents/${content.slug}`}
title={langui.content} title={langui.content}
langui={langui} langui={langui}
displayOn={ReturnButtonType.mobile} displayOn={ReturnButtonType.mobile}
className="mb-10" className="mb-10"
/> />
{content && ( <div className="grid place-items-center">
<div className="grid place-items-center"> <ThumbnailHeader
<ThumbnailHeader thumbnail={content.thumbnail?.data?.attributes}
thumbnail={content.thumbnail?.data?.attributes} pre_title={selectedTranslation?.pre_title}
pre_title={ title={selectedTranslation?.title}
selectedTitle?.pre_title ?? content.titles?.[0]?.pre_title subtitle={selectedTranslation?.subtitle}
} description={selectedTranslation?.description}
title={selectedTitle?.title ?? content.titles?.[0]?.title} type={content.type}
subtitle={selectedTitle?.subtitle ?? content.titles?.[0]?.subtitle} categories={content.categories}
description={ langui={langui}
selectedTitle?.description ?? content.titles?.[0]?.description languageSwitcher={<LanguageSwitcher />}
} />
type={content.type}
categories={content.categories}
langui={langui}
languageSwitcher={<LanguageSwitcher />}
/>
{content.previous_recommended?.data?.attributes && ( {previousContent?.attributes && (
<div className="mt-12 mb-8 w-full"> <div className="mt-12 mb-8 w-full">
<h2 className="text-center text-2xl mb-4">Previous content</h2> <h2 className="text-center text-2xl mb-4">
<PreviewLine {langui.previous_content}
href={`/contents/${content.previous_recommended.data.attributes.slug}`} </h2>
pre_title={ <PreviewLine
content.previous_recommended.data.attributes.titles?.[0] href={`/contents/${previousContent.attributes.slug}`}
?.pre_title pre_title={
} previousContent.attributes.translations?.[0]?.pre_title
title={ }
content.previous_recommended.data.attributes.titles?.[0] title={
?.title ?? previousContent.attributes.translations?.[0]?.title ??
prettySlug(content.previous_recommended.data.attributes.slug) prettySlug(previousContent.attributes.slug)
} }
subtitle={ subtitle={previousContent.attributes.translations?.[0]?.subtitle}
content.previous_recommended.data.attributes.titles?.[0] thumbnail={previousContent.attributes.thumbnail?.data?.attributes}
?.subtitle thumbnailAspectRatio="3/2"
} topChips={
thumbnail={ isMobile
content.previous_recommended.data.attributes.thumbnail?.data ? undefined
?.attributes : previousContent.attributes.type?.data?.attributes
} ? [
thumbnailAspectRatio="3/2" previousContent.attributes.type.data.attributes
topChips={ .titles?.[0]
isMobile ? previousContent.attributes.type.data.attributes
? undefined .titles[0]?.title
: content.previous_recommended.data.attributes.type?.data : prettySlug(
?.attributes previousContent.attributes.type.data.attributes.slug
? [ ),
content.previous_recommended.data.attributes.type.data ]
.attributes.titles?.[0] : undefined
? content.previous_recommended.data.attributes.type }
.data.attributes.titles[0]?.title bottomChips={
: prettySlug( isMobile
content.previous_recommended.data.attributes.type ? undefined
.data.attributes.slug : previousContent.attributes.categories?.data.map(
), (category) => category.attributes?.short ?? ""
] )
: undefined }
} />
bottomChips={ </div>
isMobile )}
? undefined
: content.previous_recommended.data.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)
}
/>
</div>
)}
<HorizontalLine /> <HorizontalLine />
<Markdawn text={selectedTextSet?.text ?? ""} /> <Markdawn text={selectedTranslation?.text_set?.text ?? ""} />
{content.next_recommended?.data?.attributes && ( {nextContent?.attributes && (
<> <>
<HorizontalLine /> <HorizontalLine />
<h2 className="text-center text-2xl mb-4">Follow-up content</h2> <h2 className="text-center text-2xl mb-4">
<PreviewLine {langui.followup_content}
href={`/contents/${content.next_recommended.data.attributes.slug}`} </h2>
pre_title={ <PreviewLine
content.next_recommended.data.attributes.titles?.[0] href={`/contents/${nextContent.attributes.slug}`}
?.pre_title pre_title={nextContent.attributes.translations?.[0]?.pre_title}
} title={
title={ nextContent.attributes.translations?.[0]?.title ??
content.next_recommended.data.attributes.titles?.[0]?.title ?? prettySlug(nextContent.attributes.slug)
prettySlug(content.next_recommended.data.attributes.slug) }
} subtitle={nextContent.attributes.translations?.[0]?.subtitle}
subtitle={ thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
content.next_recommended.data.attributes.titles?.[0]?.subtitle thumbnailAspectRatio="3/2"
} topChips={
thumbnail={ isMobile
content.next_recommended.data.attributes.thumbnail?.data ? undefined
?.attributes : nextContent.attributes.type?.data?.attributes
} ? [
thumbnailAspectRatio="3/2" nextContent.attributes.type.data.attributes.titles?.[0]
topChips={ ? nextContent.attributes.type.data.attributes.titles[0]
isMobile ?.title
? undefined : prettySlug(
: content.next_recommended.data.attributes.type?.data nextContent.attributes.type.data.attributes.slug
?.attributes ),
? [ ]
content.next_recommended.data.attributes.type.data : undefined
.attributes.titles?.[0] }
? content.next_recommended.data.attributes.type.data bottomChips={
.attributes.titles[0]?.title isMobile
: prettySlug( ? undefined
content.next_recommended.data.attributes.type.data : nextContent.attributes.categories?.data.map(
.attributes.slug (category) => category.attributes?.short ?? ""
), )
] }
: undefined />
} </>
bottomChips={ )}
isMobile </div>
? undefined
: content.next_recommended.data.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)
}
/>
</>
)}
</div>
)}
</ContentPanel> </ContentPanel>
); );
let description = ""; let description = "";
if (content?.type?.data) { if (content.type?.data) {
description += `${langui.type}: `; description += `${langui.type}: `;
description += description +=
@ -332,7 +322,7 @@ export default function Content(props: Props): JSX.Element {
description += "\n"; description += "\n";
} }
if (content?.categories?.data && content.categories.data.length > 0) { if (content.categories?.data && content.categories.data.length > 0) {
description += `${langui.categories}: `; description += `${langui.categories}: `;
description += content.categories.data description += content.categories.data
.map((category) => category.attributes?.short) .map((category) => category.attributes?.short)
@ -343,15 +333,15 @@ export default function Content(props: Props): JSX.Element {
return ( return (
<AppLayout <AppLayout
navTitle={ navTitle={
content?.titles && content.titles.length > 0 && content.titles[0] selectedTranslation
? prettyinlineTitle( ? prettyinlineTitle(
content.titles[0].pre_title, selectedTranslation.pre_title,
content.titles[0].title, selectedTranslation.title,
content.titles[0].subtitle selectedTranslation.subtitle
) )
: prettySlug(content?.slug) : prettySlug(content.slug)
} }
thumbnail={content?.thumbnail?.data?.attributes ?? undefined} thumbnail={content.thumbnail?.data?.attributes ?? undefined}
contentPanel={contentPanel} contentPanel={contentPanel}
subPanel={subPanel} subPanel={subPanel}
description={description} description={description}
@ -370,12 +360,12 @@ export async function getStaticProps(
language_code: context.locale ?? "en", language_code: context.locale ?? "en",
}); });
if (!content.contents || content.contents.data.length === 0) if (!content.contents || !content.contents.data[0].attributes?.translations) {
return { notFound: true }; return { notFound: true };
}
const props: Props = { const props: Props = {
...(await getAppStaticProps(context)), ...(await getAppStaticProps(context)),
content: content.contents.data[0].attributes, content: content.contents.data[0].attributes as ContentWithTranslations,
contentId: content.contents.data[0].id,
}; };
return { return {
props: props, props: props,

View File

@ -1,39 +1,51 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Select from "components/Inputs/Select"; import { Select } from "components/Inputs/Select";
import Switch from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import ThumbnailPreview from "components/PreviewCard"; import { PreviewCard } from "components/PreviewCard";
import { GetContentsQuery } from "graphql/generated"; import { GetContentsQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { prettyinlineTitle, prettySlug } from "queries/helpers";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
contents: Exclude<GetContentsQuery["contents"], null | undefined>["data"]; contents: NonNullable<GetContentsQuery["contents"]>["data"];
} }
type GroupContentItems = Map<string, Props["contents"]>; type GroupContentItems = Map<string, Immutable<Props["contents"]>>;
export default function Contents(props: Props): JSX.Element { export default function Contents(props: Immutable<Props>): JSX.Element {
const { langui, contents } = props; const { langui, contents } = props;
const [groupingMethod, setGroupingMethod] = useState<number>(-1); const [groupingMethod, setGroupingMethod] = useState<number>(-1);
const [keepInfoVisible, setKeepInfoVisible] = useState(false); const [keepInfoVisible, setKeepInfoVisible] = useState(false);
const [combineRelatedContent, setCombineRelatedContent] = useState(true);
const [filteredItems, setFilteredItems] = useState(
filterContents(combineRelatedContent, contents)
);
const [groups, setGroups] = useState<GroupContentItems>( const [groups, setGroups] = useState<GroupContentItems>(
getGroups(langui, groupingMethod, contents) getGroups(langui, groupingMethod, filteredItems)
); );
useEffect(() => { useEffect(() => {
setGroups(getGroups(langui, groupingMethod, contents)); setFilteredItems(filterContents(combineRelatedContent, contents));
}, [langui, groupingMethod, contents]); }, [combineRelatedContent, contents]);
useEffect(() => {
setGroups(getGroups(langui, groupingMethod, filteredItems));
}, [langui, groupingMethod, filteredItems]);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
@ -55,7 +67,15 @@ export default function Contents(props: Props): JSX.Element {
</div> </div>
<div className="flex flex-row gap-2 place-items-center coarse:hidden"> <div className="flex flex-row gap-2 place-items-center coarse:hidden">
<p className="flex-shrink-0">{"Always show info"}:</p> <p className="flex-shrink-0">{langui.combine_related_contents}:</p>
<Switch
setState={setCombineRelatedContent}
state={combineRelatedContent}
/>
</div>
<div className="flex flex-row gap-2 place-items-center coarse:hidden">
<p className="flex-shrink-0">{langui.always_show_info}:</p>
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
</div> </div>
</SubPanel> </SubPanel>
@ -69,7 +89,8 @@ export default function Contents(props: Props): JSX.Element {
{name && ( {name && (
<h2 <h2
key={`h2${name}`} key={`h2${name}`}
className="text-2xl pb-2 pt-10 first-of-type:pt-0 flex flex-row place-items-center gap-2" className="text-2xl pb-2 pt-10 first-of-type:pt-0
flex flex-row place-items-center gap-2"
> >
{name} {name}
<Chip>{`${items.length} ${ <Chip>{`${items.length} ${
@ -81,22 +102,30 @@ export default function Contents(props: Props): JSX.Element {
)} )}
<div <div
key={`items${name}`} key={`items${name}`}
className="grid gap-8 items-end grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]" className="grid gap-8 mobile:gap-4 items-end grid-cols-2
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
> >
{items.map((item) => ( {items.map((item) => (
<> <>
{item.attributes && ( {item.attributes && (
<ThumbnailPreview <PreviewCard
key={item.id} key={item.id}
href={`/contents/${item.attributes.slug}`} href={`/contents/${item.attributes.slug}`}
pre_title={item.attributes.titles?.[0]?.pre_title} pre_title={item.attributes.translations?.[0]?.pre_title}
title={ title={
item.attributes.titles?.[0]?.title ?? item.attributes.translations?.[0]?.title ??
prettySlug(item.attributes.slug) prettySlug(item.attributes.slug)
} }
subtitle={item.attributes.titles?.[0]?.subtitle} subtitle={item.attributes.translations?.[0]?.subtitle}
thumbnail={item.attributes.thumbnail?.data?.attributes} thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2" thumbnailAspectRatio="3/2"
stackNumber={
combineRelatedContent &&
item.attributes.group?.data?.attributes?.combine
? item.attributes.group.data.attributes.contents
?.data.length
: 0
}
topChips={ topChips={
item.attributes.type?.data?.attributes item.attributes.type?.data?.attributes
? [ ? [
@ -143,18 +172,18 @@ export async function getStaticProps(
}); });
if (!contents.contents) return { notFound: true }; if (!contents.contents) return { notFound: true };
contents.contents.data.sort((a, b) => { contents.contents.data.sort((a, b) => {
const titleA = a.attributes?.titles?.[0] const titleA = a.attributes?.translations?.[0]
? prettyinlineTitle( ? prettyinlineTitle(
a.attributes.titles[0].pre_title, a.attributes.translations[0].pre_title,
a.attributes.titles[0].title, a.attributes.translations[0].title,
a.attributes.titles[0].subtitle a.attributes.translations[0].subtitle
) )
: a.attributes?.slug ?? ""; : a.attributes?.slug ?? "";
const titleB = b.attributes?.titles?.[0] const titleB = b.attributes?.translations?.[0]
? prettyinlineTitle( ? prettyinlineTitle(
b.attributes.titles[0].pre_title, b.attributes.translations[0].pre_title,
b.attributes.titles[0].title, b.attributes.translations[0].title,
b.attributes.titles[0].subtitle b.attributes.translations[0].subtitle
) )
: b.attributes?.slug ?? ""; : b.attributes?.slug ?? "";
return titleA.localeCompare(titleB); return titleA.localeCompare(titleB);
@ -172,7 +201,7 @@ export async function getStaticProps(
function getGroups( function getGroups(
langui: AppStaticProps["langui"], langui: AppStaticProps["langui"],
groupByType: number, groupByType: number,
items: Props["contents"] items: Immutable<Props["contents"]>
): GroupContentItems { ): GroupContentItems {
switch (groupByType) { switch (groupByType) {
case 0: { case 0: {
@ -209,15 +238,16 @@ function getGroups(
} }
case 1: { case 1: {
const group: GroupContentItems = new Map(); const group = new Map();
items.map((item) => { items.map((item) => {
const type = const type =
item.attributes?.type?.data?.attributes?.titles?.[0]?.title ?? item.attributes?.type?.data?.attributes?.titles?.[0]?.title ??
prettySlug(item.attributes?.type?.data?.attributes?.slug); item.attributes?.type?.data?.attributes?.slug
? prettySlug(item.attributes.type.data.attributes.slug)
: langui.no_type;
if (!group.has(type)) group.set(type, []); if (!group.has(type)) group.set(type, []);
group.get(type)?.push(item); group.get(type)?.push(item);
}); });
return group; return group;
} }
@ -228,3 +258,19 @@ function getGroups(
} }
} }
} }
function filterContents(
combineRelatedContent: boolean,
contents: Immutable<Props["contents"]>
): Immutable<Props["contents"]> {
if (combineRelatedContent) {
return [...contents].filter(
(content) =>
!content.attributes?.group?.data?.attributes ||
!content.attributes.group.data.attributes.combine ||
content.attributes.group.data.attributes.contents?.data[0].id ===
content.id
);
}
return contents;
}

View File

@ -1,23 +1,22 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import ToolTip from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { import { DevGetContentsQuery } from "graphql/generated";
DevGetContentsQuery, import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
Enum_Componentsetstextset_Status,
} from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
contents: DevGetContentsQuery; contents: DevGetContentsQuery;
} }
export default function CheckupContents(props: Props): JSX.Element { export default function CheckupContents(props: Immutable<Props>): JSX.Element {
const { contents } = props; const { contents } = props;
const testReport = testingContent(contents); const testReport = testingContent(contents);
@ -38,7 +37,8 @@ export default function CheckupContents(props: Props): JSX.Element {
{testReport.lines.map((line, index) => ( {testReport.lines.map((line, index) => (
<div <div
key={index} key={index}
className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] gap-2 items-center mb-2 justify-items-start" className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr]
gap-2 items-center mb-2 justify-items-start"
> >
<Button <Button
href={line.frontendUrl} href={line.frontendUrl}
@ -112,7 +112,7 @@ type ReportLine = {
frontendUrl: string; frontendUrl: string;
}; };
function testingContent(contents: Props["contents"]): Report { function testingContent(contents: Immutable<Props["contents"]>): Report {
const report: Report = { const report: Report = {
title: "Contents", title: "Contents",
lines: [], lines: [],
@ -163,23 +163,6 @@ function testingContent(contents: Props["contents"]): Report {
}); });
} }
if (
content.attributes.next_recommended?.data?.id === content.id ||
content.attributes.previous_recommended?.data?.id === content.id
) {
report.lines.push({
subitems: [content.attributes.slug],
name: "Self Recommendation",
type: "Error",
severity: "Very High",
description:
"The Content is referring to itself as a Next or Previous Recommended.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!content.attributes.thumbnail?.data?.id) { if (!content.attributes.thumbnail?.data?.id) {
report.lines.push({ report.lines.push({
subitems: [content.attributes.slug], subitems: [content.attributes.slug],
@ -193,7 +176,7 @@ function testingContent(contents: Props["contents"]): Report {
}); });
} }
if (content.attributes.titles?.length === 0) { if (content.attributes.translations?.length === 0) {
report.lines.push({ report.lines.push({
subitems: [content.attributes.slug], subitems: [content.attributes.slug],
name: "No Titles", name: "No Titles",
@ -207,10 +190,10 @@ function testingContent(contents: Props["contents"]): Report {
} else { } else {
const titleLanguages: string[] = []; const titleLanguages: string[] = [];
content.attributes.titles?.map((title, titleIndex) => { content.attributes.translations?.map((translation, titleIndex) => {
if (title && content.attributes) { if (translation && content.attributes) {
if (title.language?.data?.id) { if (translation.language?.data?.id) {
if (title.language.data.id in titleLanguages) { if (translation.language.data.id in titleLanguages) {
report.lines.push({ report.lines.push({
subitems: [ subitems: [
content.attributes.slug, content.attributes.slug,
@ -225,7 +208,7 @@ function testingContent(contents: Props["contents"]): Report {
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} else { } else {
titleLanguages.push(title.language.data.id); titleLanguages.push(translation.language.data.id);
} }
} else { } else {
report.lines.push({ report.lines.push({
@ -242,7 +225,7 @@ function testingContent(contents: Props["contents"]): Report {
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} }
if (!title.description) { if (!translation.description) {
report.lines.push({ report.lines.push({
subitems: [ subitems: [
content.attributes.slug, content.attributes.slug,
@ -257,229 +240,199 @@ function testingContent(contents: Props["contents"]): Report {
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} }
if (translation.text_set) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Text Set",
type: "Missing",
severity: "Medium",
description: "The Content has no Text Set.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
/*
*const textSetLanguages: string[] = [];
*if (content.attributes && textSet) {
* if (textSet.language?.data?.id) {
* if (textSet.language.data.id in textSetLanguages) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Duplicate Language",
* type: "Error",
* severity: "High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* } else {
* textSetLanguages.push(textSet.language.data.id);
* }
* } else {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Language",
* type: "Error",
* severity: "Very High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (!textSet.source_language?.data?.id) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Source Language",
* type: "Error",
* severity: "High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (textSet.status !== Enum_Componentsetstextset_Status.Done) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Not Done Status",
* type: "Improvement",
* severity: "Low",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (!textSet.text || textSet.text.length < 10) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Text",
* type: "Missing",
* severity: "Medium",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (
* textSet.source_language?.data?.id ===
* textSet.language?.data?.id
* ) {
* if (textSet.transcribers?.data.length === 0) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Transcribers",
* type: "Missing",
* severity: "High",
* description:
* "The Content is a Transcription but doesn't credit any Transcribers.",
* recommandation: "Add the appropriate Transcribers.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* if (
* textSet.translators?.data &&
* textSet.translators.data.length > 0
* ) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Credited Translators",
* type: "Error",
* severity: "High",
* description:
* "The Content is a Transcription but credits one or more Translators.",
* recommandation:
* "If appropriate, create a Translation Text Set with the Translator credited there.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* } else {
* if (textSet.translators?.data.length === 0) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Translators",
* type: "Missing",
* severity: "High",
* description:
* "The Content is a Transcription but doesn't credit any Translators.",
* recommandation: "Add the appropriate Translators.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* if (
* textSet.transcribers?.data &&
* textSet.transcribers.data.length > 0
* ) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Credited Transcribers",
* type: "Error",
* severity: "High",
* description:
* "The Content is a Translation but credits one or more Transcribers.",
* recommandation:
* "If appropriate, create a Transcription Text Set with the Transcribers credited there.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* }
*}
*/
}
report.lines.push({
subitems: [content.attributes.slug],
name: "No Sets",
type: "Missing",
severity: "Medium",
description: "The Content has no Sets.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} }
}); });
} }
if (
content.attributes.text_set?.length === 0 &&
content.attributes.audio_set?.length === 0 &&
content.attributes.video_set?.length === 0
) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Sets",
type: "Missing",
severity: "Medium",
description: "The Content has no Sets.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
if (content.attributes.video_set?.length === 0) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Video Sets",
type: "Missing",
severity: "Very Low",
description: "The Content has no Video Sets.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (content.attributes.audio_set?.length === 0) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Audio Sets",
type: "Missing",
severity: "Very Low",
description: "The Content has no Audio Sets.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (content.attributes.text_set?.length === 0) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Text Set",
type: "Missing",
severity: "Medium",
description: "The Content has no Text Set.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
const textSetLanguages: string[] = [];
content.attributes.text_set?.map((textSet, textSetIndex) => {
if (content.attributes && textSet) {
if (textSet.language?.data?.id) {
if (textSet.language.data.id in textSetLanguages) {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "Duplicate Language",
type: "Error",
severity: "High",
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
textSetLanguages.push(textSet.language.data.id);
}
} else {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "No Language",
type: "Error",
severity: "Very High",
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!textSet.source_language?.data?.id) {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "No Source Language",
type: "Error",
severity: "High",
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (textSet.status !== Enum_Componentsetstextset_Status.Done) {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "Not Done Status",
type: "Improvement",
severity: "Low",
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!textSet.text || textSet.text.length < 10) {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "No Text",
type: "Missing",
severity: "Medium",
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (
textSet.source_language?.data?.id === textSet.language?.data?.id
) {
if (textSet.transcribers?.data.length === 0) {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "No Transcribers",
type: "Missing",
severity: "High",
description:
"The Content is a Transcription but doesn't credit any Transcribers.",
recommandation: "Add the appropriate Transcribers.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (
textSet.translators?.data &&
textSet.translators.data.length > 0
) {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "Credited Translators",
type: "Error",
severity: "High",
description:
"The Content is a Transcription but credits one or more Translators.",
recommandation:
"If appropriate, create a Translation Text Set with the Translator credited there.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
} else {
if (textSet.translators?.data.length === 0) {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "No Translators",
type: "Missing",
severity: "High",
description:
"The Content is a Transcription but doesn't credit any Translators.",
recommandation: "Add the appropriate Translators.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (
textSet.transcribers?.data &&
textSet.transcribers.data.length > 0
) {
report.lines.push({
subitems: [
content.attributes.slug,
`TextSet ${textSetIndex.toString()}`,
],
name: "Credited Transcribers",
type: "Error",
severity: "High",
description:
"The Content is a Translation but credits one or more Transcribers.",
recommandation:
"If appropriate, create a Transcription Text Set with the Transcribers credited there.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
}
}
});
}
}
} }
}); });
return report; return report;

View File

@ -1,23 +1,27 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import ToolTip from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { import {
DevGetLibraryItemsQuery, DevGetLibraryItemsQuery,
Enum_Componentcollectionscomponentlibraryimages_Status, Enum_Componentcollectionscomponentlibraryimages_Status,
} from "graphql/generated"; } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
libraryItems: DevGetLibraryItemsQuery; libraryItems: DevGetLibraryItemsQuery;
} }
export default function CheckupLibraryItems(props: Props): JSX.Element { export default function CheckupLibraryItems(
props: Immutable<Props>
): JSX.Element {
const { libraryItems } = props; const { libraryItems } = props;
const testReport = testingLibraryItem(libraryItems); const testReport = testingLibraryItem(libraryItems);
@ -38,7 +42,8 @@ export default function CheckupLibraryItems(props: Props): JSX.Element {
{testReport.lines.map((line, index) => ( {testReport.lines.map((line, index) => (
<div <div
key={index} key={index}
className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] gap-2 items-center mb-2 justify-items-start" className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr]
gap-2 items-center mb-2 justify-items-start"
> >
<Button <Button
href={line.frontendUrl} href={line.frontendUrl}
@ -113,7 +118,9 @@ type ReportLine = {
}; };
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
function testingLibraryItem(libraryItems: Props["libraryItems"]): Report { function testingLibraryItem(
libraryItems: Immutable<Props["libraryItems"]>
): Report {
const report: Report = { const report: Report = {
title: "Contents", title: "Contents",
lines: [], lines: [],

View File

@ -1,19 +1,21 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import Markdawn from "components/Markdown/Markdawn"; import { Markdawn } from "components/Markdown/Markdawn";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import Popup from "components/Popup"; import { Popup } from "components/Popup";
import ToolTip from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import TurndownService from "turndown"; import TurndownService from "turndown";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Editor(props: Props): JSX.Element { export default function Editor(props: Immutable<Props>): JSX.Element {
const handleInput = useCallback((text: string) => { const handleInput = useCallback((text: string) => {
setMarkdown(text); setMarkdown(text);
}, []); }, []);
@ -337,7 +339,8 @@ export default function Editor(props: Props): JSX.Element {
const textarea = event.target as HTMLTextAreaElement; const textarea = event.target as HTMLTextAreaElement;
handleInput(textarea.value); handleInput(textarea.value);
}} }}
className="bg-mid !bg-opacity-40 rounded-xl outline-none p-8 w-full text-black font-monospace h-[70vh]" className="bg-mid !bg-opacity-40 rounded-xl
outline-none p-8 w-full text-black font-monospace h-[70vh]"
value={markdown} value={markdown}
title="Input textarea" title="Input textarea"
/> />

View File

@ -1,10 +1,11 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Gallery(props: Props): JSX.Element { export default function Gallery(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const contentPanel = ( const contentPanel = (
<iframe <iframe

View File

@ -1,13 +1,11 @@
import PostPage, { Post } from "components/PostPage"; import { PostPage } from "components/PostPage";
import { getReadySdk } from "graphql/sdk"; import {
import { GetStaticPropsContext } from "next"; getPostStaticProps,
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; PostStaticProps,
} from "graphql/getPostStaticProps";
import { Immutable } from "helpers/types";
interface Props extends AppStaticProps { export default function Home(props: Immutable<PostStaticProps>): JSX.Element {
post: Post;
}
export default function Home(props: Props): JSX.Element {
const { post, langui, languages, currencies } = props; const { post, langui, languages, currencies } = props;
return ( return (
<PostPage <PostPage
@ -17,7 +15,11 @@ export default function Home(props: Props): JSX.Element {
post={post} post={post}
prependBody={ prependBody={
<div className="grid place-items-center place-content-center w-full gap-5 text-center"> <div className="grid place-items-center place-content-center w-full gap-5 text-center">
<div className="[mask:url('/icons/accords.svg')] [mask-size:contain] [mask-repeat:no-repeat] [mask-position:center] w-32 aspect-square mobile:w-[50vw] bg-black" /> <div
className="[mask:url('/icons/accords.svg')] [mask-size:contain]
[mask-repeat:no-repeat] [mask-position:center] w-32 aspect-square
mobile:w-[50vw] bg-black"
/>
<h1 className="text-5xl mb-0">Accord&rsquo;s Library</h1> <h1 className="text-5xl mb-0">Accord&rsquo;s Library</h1>
<h2 className="text-xl -mt-5"> <h2 className="text-xl -mt-5">
Discover Analyze Translate Archive Discover Analyze Translate Archive
@ -30,21 +32,4 @@ export default function Home(props: Props): JSX.Element {
); );
} }
export async function getStaticProps( export const getStaticProps = getPostStaticProps("home");
context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
const slug = "home";
const post = await sdk.getPost({
slug: slug,
language_code: context.locale ?? "en",
});
if (!post.posts?.data[0].attributes) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
post: post.posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -1,57 +1,59 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Img, { getAssetURL, ImageQuality } from "components/Img"; import { Img } from "components/Img";
import Button from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import Switch from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
import InsetBox from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import ContentLine from "components/Library/ContentLine"; import { ContentLine } from "components/Library/ContentLine";
import LightBox from "components/LightBox"; import { NavOption } from "components/PanelComponents/NavOption";
import NavOption from "components/PanelComponents/NavOption"; import {
import ReturnButton, { ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import ThumbnailPreview from "components/PreviewCard"; import { PreviewCard } from "components/PreviewCard";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { import {
Enum_Componentmetadatabooks_Binding_Type, Enum_Componentmetadatabooks_Binding_Type,
Enum_Componentmetadatabooks_Page_Order, Enum_Componentmetadatabooks_Page_Order,
GetLibraryItemQuery, GetLibraryItemQuery,
} from "graphql/generated"; } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import {
convertMmToInch,
prettyDate, prettyDate,
prettyinlineTitle, prettyinlineTitle,
prettyItemSubType, prettyItemSubType,
prettyItemType, prettyItemType,
prettyPrice, prettyPrice,
prettyURL, prettyURL,
sortContent, } from "helpers/formatters";
} from "queries/helpers"; import { getAssetURL, ImageQuality } from "helpers/img";
import { convertMmToInch } from "helpers/numbers";
import { sortContent } from "helpers/others";
import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox";
import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { useState } from "react"; import { useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
item: Exclude< item: NonNullable<
GetLibraryItemQuery["libraryItems"], GetLibraryItemQuery["libraryItems"]
null | undefined
>["data"][number]["attributes"]; >["data"][number]["attributes"];
itemId: Exclude< itemId: NonNullable<
GetLibraryItemQuery["libraryItems"], GetLibraryItemQuery["libraryItems"]
null | undefined
>["data"][number]["id"]; >["data"][number]["id"];
} }
export default function LibrarySlug(props: Props): JSX.Element { export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
const { item, langui, currencies } = props; const { item, langui, currencies } = props;
const appLayout = useAppLayout(); const appLayout = useAppLayout();
@ -61,10 +63,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
sortContent(item?.contents); sortContent(item?.contents);
const [lightboxOpen, setLightboxOpen] = useState(false); const [openLightBox, LightBox] = useLightBox();
const [lightboxImages, setLightboxImages] = useState([""]);
const [lightboxIndex, setLightboxIndex] = useState(0);
const [keepInfoVisible, setKeepInfoVisible] = useState(false); const [keepInfoVisible, setKeepInfoVisible] = useState(false);
let displayOpenScans = false; let displayOpenScans = false;
@ -134,13 +133,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
const contentPanel = ( const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}> <ContentPanel width={ContentPanelWidthSizes.large}>
<LightBox <LightBox />
state={lightboxOpen}
setState={setLightboxOpen}
images={lightboxImages}
index={lightboxIndex}
setIndex={setLightboxIndex}
/>
<ReturnButton <ReturnButton
href="/library/" href="/library/"
@ -151,17 +144,16 @@ export default function LibrarySlug(props: Props): JSX.Element {
/> />
<div className="grid place-items-center gap-12"> <div className="grid place-items-center gap-12">
<div <div
className="drop-shadow-shade-xl w-full h-[50vh] mobile:h-[60vh] desktop:mb-16 relative cursor-pointer" className="drop-shadow-shade-xl w-full h-[50vh]
mobile:h-[60vh] desktop:mb-16 relative cursor-pointer"
onClick={() => { onClick={() => {
if (item?.thumbnail?.data?.attributes) { if (item?.thumbnail?.data?.attributes) {
setLightboxOpen(true); openLightBox([
setLightboxImages([
getAssetURL( getAssetURL(
item.thumbnail.data.attributes.url, item.thumbnail.data.attributes.url,
ImageQuality.Large ImageQuality.Large
), ),
]); ]);
setLightboxIndex(0);
} }
}} }}
> >
@ -169,9 +161,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
<Img <Img
image={item.thumbnail.data.attributes} image={item.thumbnail.data.attributes}
quality={ImageQuality.Large} quality={ImageQuality.Large}
layout="fill" className="w-full h-full object-contain"
objectFit="contain"
priority
/> />
) : ( ) : (
<div className="w-full aspect-[21/29.7] bg-light rounded-xl"></div> <div className="w-full aspect-[21/29.7] bg-light rounded-xl"></div>
@ -212,7 +202,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
<> <>
{item?.urls && item.urls.length ? ( {item?.urls && item.urls.length ? (
<div className="flex flex-row place-items-center gap-3"> <div className="flex flex-row place-items-center gap-3">
<p>Available at</p> <p>{langui.available_at}</p>
{item.urls.map((url) => ( {item.urls.map((url) => (
<> <>
{url?.url && ( {url?.url && (
@ -228,7 +218,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
))} ))}
</div> </div>
) : ( ) : (
<p>This item is not for sale or is no longer available</p> <p>{langui.item_not_available}</p>
)} )}
</> </>
)} )}
@ -238,13 +228,17 @@ export default function LibrarySlug(props: Props): JSX.Element {
{item?.gallery && item.gallery.data.length > 0 && ( {item?.gallery && item.gallery.data.length > 0 && (
<div id="gallery" className="grid place-items-center gap-8 w-full"> <div id="gallery" className="grid place-items-center gap-8 w-full">
<h2 className="text-2xl">{langui.gallery}</h2> <h2 className="text-2xl">{langui.gallery}</h2>
<div className="grid w-full gap-8 items-end grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"> <div
className="grid w-full gap-8 items-end
grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
>
{item.gallery.data.map((galleryItem, index) => ( {item.gallery.data.map((galleryItem, index) => (
<> <>
{galleryItem.attributes && ( {galleryItem.attributes && (
<div <div
key={galleryItem.id} key={galleryItem.id}
className="relative aspect-square hover:scale-[1.02] transition-transform cursor-pointer" className="relative aspect-square hover:scale-[1.02]
transition-transform cursor-pointer"
onClick={() => { onClick={() => {
if (item.gallery?.data) { if (item.gallery?.data) {
const images: string[] = []; const images: string[] = [];
@ -257,18 +251,14 @@ export default function LibrarySlug(props: Props): JSX.Element {
) )
); );
}); });
setLightboxOpen(true); openLightBox(images, index);
setLightboxImages(images);
setLightboxIndex(index);
} }
}} }}
> >
<div className="bg-light absolute inset-0 rounded-lg drop-shadow-shade-md"></div>
<Img <Img
className="rounded-lg" className="bg-light rounded-lg drop-shadow-shade-md
w-full h-full object-cover"
image={galleryItem.attributes} image={galleryItem.attributes}
layout="fill"
objectFit="cover"
/> />
</div> </div>
)} )}
@ -425,14 +415,17 @@ export default function LibrarySlug(props: Props): JSX.Element {
</h2> </h2>
<div className="-mt-6 mb-8 flex flex-row gap-2 place-items-center coarse:hidden"> <div className="-mt-6 mb-8 flex flex-row gap-2 place-items-center coarse:hidden">
<p className="flex-shrink-0">{"Always show info"}:</p> <p className="flex-shrink-0">{langui.always_show_info}:</p>
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
</div> </div>
<div className="grid gap-8 items-end mobile:grid-cols-2 grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] w-full"> <div
className="grid gap-8 items-end mobile:grid-cols-2
grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] w-full"
>
{item.subitems.data.map((subitem) => ( {item.subitems.data.map((subitem) => (
<> <>
{subitem.attributes && ( {subitem.attributes && (
<ThumbnailPreview <PreviewCard
key={subitem.id} key={subitem.id}
href={`/library/${subitem.attributes.slug}`} href={`/library/${subitem.attributes.slug}`}
title={subitem.attributes.title} title={subitem.attributes.title}

View File

@ -1,47 +1,46 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import ScanSet from "components/Library/ScanSet"; import { ScanSet } from "components/Library/ScanSet";
import ScanSetCover from "components/Library/ScanSetCover"; import { ScanSetCover } from "components/Library/ScanSetCover";
import LightBox from "components/LightBox"; import { NavOption } from "components/PanelComponents/NavOption";
import NavOption from "components/PanelComponents/NavOption"; import {
import ReturnButton, { ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { GetLibraryItemScansQuery } from "graphql/generated"; import { GetLibraryItemScansQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { sortContent } from "helpers/others";
import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox";
import { import {
GetStaticPathsContext, GetStaticPathsContext,
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { prettyinlineTitle, prettySlug, sortContent } from "queries/helpers";
import { useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
item: Exclude< item: NonNullable<
GetLibraryItemScansQuery["libraryItems"], GetLibraryItemScansQuery["libraryItems"]
null | undefined
>["data"][number]["attributes"]; >["data"][number]["attributes"];
itemId: Exclude< itemId: NonNullable<
GetLibraryItemScansQuery["libraryItems"], GetLibraryItemScansQuery["libraryItems"]
null | undefined
>["data"][number]["id"]; >["data"][number]["id"];
} }
export default function LibrarySlug(props: Props): JSX.Element { export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
const { item, langui, languages } = props; const { item, langui, languages } = props;
const appLayout = useAppLayout(); const appLayout = useAppLayout();
sortContent(item?.contents); sortContent(item?.contents);
const [lightboxOpen, setLightboxOpen] = useState(false); const [openLightBox, LightBox] = useLightBox();
const [lightboxImages, setLightboxImages] = useState([""]);
const [lightboxIndex, setLightboxIndex] = useState(0);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
@ -61,7 +60,9 @@ export default function LibrarySlug(props: Props): JSX.Element {
subtitle={ subtitle={
content.attributes?.range[0]?.__typename === content.attributes?.range[0]?.__typename ===
"ComponentRangePageRange" "ComponentRangePageRange"
? `${content.attributes.range[0].starting_page}${content.attributes.range[0].ending_page}` ? `${content.attributes.range[0].starting_page}` +
`` +
`${content.attributes.range[0].ending_page}`
: undefined : undefined
} }
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => appLayout.setSubPanelOpen(false)}
@ -73,13 +74,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
const contentPanel = ( const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}> <ContentPanel width={ContentPanelWidthSizes.large}>
<LightBox <LightBox />
state={lightboxOpen}
setState={setLightboxOpen}
images={lightboxImages}
index={lightboxIndex}
setIndex={setLightboxIndex}
/>
<ReturnButton <ReturnButton
href={`/library/${item?.slug}`} href={`/library/${item?.slug}`}
@ -92,9 +87,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
{item?.images && ( {item?.images && (
<ScanSetCover <ScanSetCover
images={item.images} images={item.images}
setLightboxImages={setLightboxImages} openLightBox={openLightBox}
setLightboxIndex={setLightboxIndex}
setLightboxOpen={setLightboxOpen}
languages={languages} languages={languages}
langui={langui} langui={langui}
/> />
@ -106,9 +99,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
<ScanSet <ScanSet
key={content.id} key={content.id}
scanSet={content.attributes.scan_set} scanSet={content.attributes.scan_set}
setLightboxImages={setLightboxImages} openLightBox={openLightBox}
setLightboxIndex={setLightboxIndex}
setLightboxOpen={setLightboxOpen}
slug={content.attributes.slug} slug={content.attributes.slug}
title={prettySlug(content.attributes.slug, item.slug)} title={prettySlug(content.attributes.slug, item.slug)}
languages={languages} languages={languages}

View File

@ -1,35 +1,34 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Chip from "components/Chip"; import { Chip } from "components/Chip";
import Select from "components/Inputs/Select"; import { Select } from "components/Inputs/Select";
import Switch from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import ThumbnailPreview from "components/PreviewCard"; import { PreviewCard } from "components/PreviewCard";
import { GetLibraryItemsPreviewQuery } from "graphql/generated"; import { GetLibraryItemsPreviewQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { import {
convertPrice,
prettyDate, prettyDate,
prettyinlineTitle, prettyinlineTitle,
prettyItemSubType, prettyItemSubType,
} from "queries/helpers"; } from "helpers/formatters";
import { convertPrice } from "helpers/numbers";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
items: Exclude< items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
GetLibraryItemsPreviewQuery["libraryItems"],
null | undefined
>["data"];
} }
type GroupLibraryItems = Map<string, Props["items"]>; type GroupLibraryItems = Map<string, Immutable<Props["items"]>>;
export default function Library(props: Props): JSX.Element { export default function Library(props: Immutable<Props>): JSX.Element {
const { langui, items: libraryItems, currencies } = props; const { langui, items: libraryItems, currencies } = props;
const [showSubitems, setShowSubitems] = useState<boolean>(false); const [showSubitems, setShowSubitems] = useState<boolean>(false);
@ -39,7 +38,7 @@ export default function Library(props: Props): JSX.Element {
const [groupingMethod, setGroupingMethod] = useState<number>(-1); const [groupingMethod, setGroupingMethod] = useState<number>(-1);
const [keepInfoVisible, setKeepInfoVisible] = useState(false); const [keepInfoVisible, setKeepInfoVisible] = useState(false);
const [filteredItems, setFilteredItems] = useState<Props["items"]>( const [filteredItems, setFilteredItems] = useState(
filterItems( filterItems(
showSubitems, showSubitems,
showPrimaryItems, showPrimaryItems,
@ -48,11 +47,11 @@ export default function Library(props: Props): JSX.Element {
) )
); );
const [sortedItems, setSortedItem] = useState<Props["items"]>( const [sortedItems, setSortedItem] = useState(
sortBy(groupingMethod, filteredItems, currencies) sortBy(groupingMethod, filteredItems, currencies)
); );
const [groups, setGroups] = useState<GroupLibraryItems>( const [groups, setGroups] = useState(
getGroups(langui, groupingMethod, sortedItems) getGroups(langui, groupingMethod, sortedItems)
); );
@ -128,7 +127,7 @@ export default function Library(props: Props): JSX.Element {
</div> </div>
<div className="flex flex-row gap-2 place-items-center coarse:hidden"> <div className="flex flex-row gap-2 place-items-center coarse:hidden">
<p className="flex-shrink-0">{"Always show info"}:</p> <p className="flex-shrink-0">{langui.always_show_info}:</p>
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
</div> </div>
</SubPanel> </SubPanel>
@ -142,7 +141,8 @@ export default function Library(props: Props): JSX.Element {
{name && ( {name && (
<h2 <h2
key={`h2${name}`} key={`h2${name}`}
className="text-2xl pb-2 pt-10 first-of-type:pt-0 flex flex-row place-items-center gap-2" className="text-2xl pb-2 pt-10 first-of-type:pt-0
flex flex-row place-items-center gap-2"
> >
{name} {name}
<Chip>{`${items.length} ${ <Chip>{`${items.length} ${
@ -154,12 +154,14 @@ export default function Library(props: Props): JSX.Element {
)} )}
<div <div
key={`items${name}`} key={`items${name}`}
className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0" className="grid gap-8 mobile:gap-4 items-end mobile:grid-cols-2
desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]
pb-12 border-b-[3px] border-dotted last-of-type:border-0"
> >
{items.map((item) => ( {items.map((item) => (
<> <>
{item.attributes && ( {item.attributes && (
<ThumbnailPreview <PreviewCard
key={item.id} key={item.id}
href={`/library/${item.attributes.slug}`} href={`/library/${item.attributes.slug}`}
title={item.attributes.title} title={item.attributes.title}
@ -224,7 +226,7 @@ export async function getStaticProps(
function getGroups( function getGroups(
langui: AppStaticProps["langui"], langui: AppStaticProps["langui"],
groupByType: number, groupByType: number,
items: Props["items"] items: Immutable<Props["items"]>
): GroupLibraryItems { ): GroupLibraryItems {
switch (groupByType) { switch (groupByType) {
case 0: { case 0: {
@ -262,7 +264,7 @@ function getGroups(
} }
case 1: { case 1: {
const group: GroupLibraryItems = new Map(); const group = new Map();
group.set(langui.audio ?? "Audio", []); group.set(langui.audio ?? "Audio", []);
group.set(langui.game ?? "Game", []); group.set(langui.game ?? "Game", []);
group.set(langui.textual ?? "Textual", []); group.set(langui.textual ?? "Textual", []);
@ -334,7 +336,7 @@ function getGroups(
years.push(item.attributes.release_date.year); years.push(item.attributes.release_date.year);
} }
}); });
const group: GroupLibraryItems = new Map(); const group = new Map();
years.sort((a, b) => a - b); years.sort((a, b) => a - b);
years.map((year) => { years.map((year) => {
group.set(year.toString(), []); group.set(year.toString(), []);
@ -352,7 +354,7 @@ function getGroups(
} }
default: { default: {
const group: GroupLibraryItems = new Map(); const group = new Map();
group.set("", items); group.set("", items);
return group; return group;
} }
@ -363,8 +365,8 @@ function filterItems(
showSubitems: boolean, showSubitems: boolean,
showPrimaryItems: boolean, showPrimaryItems: boolean,
showSecondaryItems: boolean, showSecondaryItems: boolean,
items: Props["items"] items: Immutable<Props["items"]>
): Props["items"] { ): Immutable<Props["items"]> {
return [...items].filter((item) => { return [...items].filter((item) => {
if (!showSubitems && !item.attributes?.root_item) return false; if (!showSubitems && !item.attributes?.root_item) return false;
if ( if (
@ -384,9 +386,9 @@ function filterItems(
function sortBy( function sortBy(
orderByType: number, orderByType: number,
items: Props["items"], items: Immutable<Props["items"]>,
currencies: AppStaticProps["currencies"] currencies: AppStaticProps["currencies"]
): Props["items"] { ): Immutable<Props["items"]> {
switch (orderByType) { switch (orderByType) {
case 0: case 0:
return [...items].sort((a, b) => { return [...items].sort((a, b) => {

View File

@ -1,11 +1,12 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Merch(props: Props): JSX.Element { export default function Merch(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>

View File

@ -1,22 +1,20 @@
import PostPage, { Post } from "components/PostPage"; import { PostPage } from "components/PostPage";
import { GetPostQuery } from "graphql/generated"; import { AppStaticProps } from "graphql/getAppStaticProps";
import {
getPostStaticProps,
PostStaticProps,
} from "graphql/getPostStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { Immutable } from "helpers/types";
import { import {
GetStaticPathsContext, GetStaticPathsContext,
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps { interface Props extends AppStaticProps, PostStaticProps {}
post: Post;
postId: Exclude<
GetPostQuery["posts"],
null | undefined
>["data"][number]["id"];
}
export default function LibrarySlug(props: Props): JSX.Element { export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
const { post, langui, languages, currencies } = props; const { post, langui, languages, currencies } = props;
return ( return (
<PostPage <PostPage
@ -36,21 +34,8 @@ export default function LibrarySlug(props: Props): JSX.Element {
export async function getStaticProps( export async function getStaticProps(
context: GetStaticPropsContext context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> { ): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
const slug = context.params?.slug ? context.params.slug.toString() : ""; const slug = context.params?.slug ? context.params.slug.toString() : "";
const post = await sdk.getPost({ return await getPostStaticProps(slug)(context);
slug: slug,
language_code: context.locale ?? "en",
});
if (!post.posts?.data[0].attributes) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
post: post.posts.data[0].attributes,
postId: post.posts.data[0].id,
};
return {
props: props,
};
} }
export async function getStaticPaths( export async function getStaticPaths(

View File

@ -1,35 +1,30 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import Switch from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import ContentPanel, { import {
ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import ThumbnailPreview from "components/PreviewCard"; import { PreviewCard } from "components/PreviewCard";
import { GetPostsPreviewQuery } from "graphql/generated"; import { GetPostsPreviewQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettySlug } from "helpers/formatters";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { prettyDate, prettySlug } from "queries/helpers";
import { useState } from "react"; import { useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
posts: Exclude<GetPostsPreviewQuery["posts"], null | undefined>["data"]; posts: NonNullable<GetPostsPreviewQuery["posts"]>["data"];
} }
export default function News(props: Props): JSX.Element { export default function News(props: Immutable<Props>): JSX.Element {
const { langui, posts } = props; const { langui } = props;
const posts = sortPosts(props.posts);
const [keepInfoVisible, setKeepInfoVisible] = useState(true); const [keepInfoVisible, setKeepInfoVisible] = useState(true);
posts
.sort((a, b) => {
const dateA = a.attributes?.date ? prettyDate(a.attributes.date) : "9999";
const dateB = b.attributes?.date ? prettyDate(b.attributes.date) : "9999";
return dateA.localeCompare(dateB);
})
.reverse();
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
<PanelHeader <PanelHeader
@ -39,7 +34,7 @@ export default function News(props: Props): JSX.Element {
/> />
<div className="flex flex-row gap-2 place-items-center coarse:hidden"> <div className="flex flex-row gap-2 place-items-center coarse:hidden">
<p className="flex-shrink-0">{"Always show info"}:</p> <p className="flex-shrink-0">{langui.always_show_info}:</p>
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
</div> </div>
</SubPanel> </SubPanel>
@ -47,11 +42,14 @@ export default function News(props: Props): JSX.Element {
const contentPanel = ( const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}> <ContentPanel width={ContentPanelWidthSizes.large}>
<div className="grid gap-8 items-end grid-cols-1 desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]"> <div
className="grid gap-8 items-end grid-cols-1
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]"
>
{posts.map((post) => ( {posts.map((post) => (
<> <>
{post.attributes && ( {post.attributes && (
<ThumbnailPreview <PreviewCard
key={post.id} key={post.id}
href={`/news/${post.attributes.slug}`} href={`/news/${post.attributes.slug}`}
title={ title={
@ -103,3 +101,17 @@ export async function getStaticProps(
props: props, props: props,
}; };
} }
function sortPosts(
posts: Immutable<Props["posts"]>
): Immutable<Props["posts"]> {
const sortedPosts = [...posts] as Props["posts"];
sortedPosts
.sort((a, b) => {
const dateA = a.attributes?.date ? prettyDate(a.attributes.date) : "9999";
const dateB = b.attributes?.date ? prettyDate(b.attributes.date) : "9999";
return dateA.localeCompare(dateB);
})
.reverse();
return sortedPosts as Immutable<Props["posts"]>;
}

View File

@ -1,28 +1,25 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import InsetBox from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import NavOption from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
import ReturnButton, { import {
ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel"; import { ContentPanel } from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import ChronologyYearComponent from "components/Wiki/Chronology/ChronologyYearComponent"; import { ChronologyYearComponent } from "components/Wiki/Chronology/ChronologyYearComponent";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { GetChronologyItemsQuery, GetErasQuery } from "graphql/generated"; import { GetChronologyItemsQuery, GetErasQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { prettySlug } from "queries/helpers";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
chronologyItems: Exclude< chronologyItems: NonNullable<
GetChronologyItemsQuery["chronologyItems"], GetChronologyItemsQuery["chronologyItems"]
null | undefined
>["data"];
chronologyEras: Exclude<
GetErasQuery["chronologyEras"],
null | undefined
>["data"]; >["data"];
chronologyEras: NonNullable<GetErasQuery["chronologyEras"]>["data"];
} }
export default function Chronology(props: Props): JSX.Element { export default function Chronology(props: Props): JSX.Element {

View File

@ -1,13 +1,14 @@
import AppLayout from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import NavOption from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
import PanelHeader from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
export default function Wiki(props: Props): JSX.Element { export default function Wiki(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>

View File

@ -159,5 +159,13 @@ module.exports = {
}, },
}); });
}), }),
plugin(function ({ addUtilities }) {
addUtilities({
".break-words": {
"word-break": "break-word",
},
});
}),
], ],
}; };