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-depth": ["warn", 4],
// "max-lines": "warn",
"max-len": ["warn", { code: 100 }],
// "max-lines-per-function": "warn",
// "max-nested-callbacks": "warn",
// "max-params": "warn",

View File

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

2245
package-lock.json generated

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { AppStaticProps } from "queries/getAppStaticProps";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { Immutable } from "helpers/types";
interface Props {
year: number;
items: Exclude<
GetChronologyItemsQuery["chronologyItems"],
null | undefined
items: NonNullable<
GetChronologyItemsQuery["chronologyItems"]
>["data"][number][];
langui: AppStaticProps["langui"];
}
export default function ChronologyYearComponent(props: Props): JSX.Element {
export function ChronologyYearComponent(props: Immutable<Props>): JSX.Element {
const { langui } = props;
return (

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

88
src/helpers/img.ts Normal file
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 { usePrefersDarkMode } from "./useMediaQuery";
import useStateWithLocalStorage from "./useStateWithLocalStorage";
import { useStateWithLocalStorage } from "./useStateWithLocalStorage";
export default function useDarkMode(
export function useDarkMode(
key: string,
initialValue: boolean | undefined
): [

28
src/hooks/useLightBox.tsx Normal file
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";
export default function useMediaQuery(query: string): boolean {
export function useMediaQuery(query: string): boolean {
function getMatches(query: string): boolean {
// Prevents SSR issues
if (typeof window !== "undefined") {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {}
export default function Gallery(props: Props): JSX.Element {
export default function Gallery(props: Immutable<Props>): JSX.Element {
const { langui } = props;
const contentPanel = (
<iframe

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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