Added reader
|
@ -19,10 +19,12 @@
|
|||
"meilisearch": "^0.27.0",
|
||||
"next": "^12.3.0",
|
||||
"nodemailer": "^6.7.8",
|
||||
"rc-slider": "^10.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hot-keys": "^2.7.2",
|
||||
"react-swipeable": "^7.0.0",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"string-natural-compare": "^3.0.1",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
|
@ -979,9 +981,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz",
|
||||
"integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz",
|
||||
"integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
},
|
||||
|
@ -3675,6 +3677,11 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||
},
|
||||
"node_modules/clean-stack": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
|
@ -7676,6 +7683,38 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/rc-slider": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.0.1.tgz",
|
||||
"integrity": "sha512-igTKF3zBet7oS/3yNiIlmU8KnZ45npmrmHlUUio8PNbIhzMcsh+oE/r2UD42Y6YD2D/s+kzCQkzQrPD6RY435Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
"classnames": "^2.2.5",
|
||||
"rc-util": "^5.18.1",
|
||||
"shallowequal": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rc-util": {
|
||||
"version": "5.24.4",
|
||||
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.24.4.tgz",
|
||||
"integrity": "sha512-2a4RQnycV9eV7lVZPEJ7QwJRPlZNc06J7CwcwZo4vIHr3PfUqtYgl1EkUV9ETAc6VRRi8XZOMFhYG63whlIC9Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"react-is": "^16.12.0",
|
||||
"shallowequal": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
|
@ -7726,6 +7765,19 @@
|
|||
"react": "^16.8.3 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-zoom-pan-pinch": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-2.1.3.tgz",
|
||||
"integrity": "sha512-a5AChOWhjo0RmxsNZXGQIlNh3e3nLU6m4V6M+6dlbPNk5d+MtMxgKWyA5zpR06Lp3OZkZVF9nR8JeWSvKwck9g==",
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -8043,6 +8095,11 @@
|
|||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
@ -9755,9 +9812,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz",
|
||||
"integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz",
|
||||
"integrity": "sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
|
@ -11860,6 +11917,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"classnames": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||
},
|
||||
"clean-stack": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
|
@ -14751,6 +14813,27 @@
|
|||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true
|
||||
},
|
||||
"rc-slider": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.0.1.tgz",
|
||||
"integrity": "sha512-igTKF3zBet7oS/3yNiIlmU8KnZ45npmrmHlUUio8PNbIhzMcsh+oE/r2UD42Y6YD2D/s+kzCQkzQrPD6RY435Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
"classnames": "^2.2.5",
|
||||
"rc-util": "^5.18.1",
|
||||
"shallowequal": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"rc-util": {
|
||||
"version": "5.24.4",
|
||||
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.24.4.tgz",
|
||||
"integrity": "sha512-2a4RQnycV9eV7lVZPEJ7QwJRPlZNc06J7CwcwZo4vIHr3PfUqtYgl1EkUV9ETAc6VRRi8XZOMFhYG63whlIC9Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"react-is": "^16.12.0",
|
||||
"shallowequal": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
|
@ -14788,6 +14871,12 @@
|
|||
"integrity": "sha512-NI7KGfQ6gwNFN0Hor3vytYW3iRfMMaivGEuxcADOOfBCx/kqwXE8IfHFxEcxSUkxCYf38COLKYd9EMYZghqaUA==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-zoom-pan-pinch": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-2.1.3.tgz",
|
||||
"integrity": "sha512-a5AChOWhjo0RmxsNZXGQIlNh3e3nLU6m4V6M+6dlbPNk5d+MtMxgKWyA5zpR06Lp3OZkZVF9nR8JeWSvKwck9g==",
|
||||
"requires": {}
|
||||
},
|
||||
"read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -15026,6 +15115,11 @@
|
|||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"dev": true
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
|
@ -30,10 +30,12 @@
|
|||
"meilisearch": "^0.27.0",
|
||||
"next": "^12.3.0",
|
||||
"nodemailer": "^6.7.8",
|
||||
"rc-slider": "^10.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hot-keys": "^2.7.2",
|
||||
"react-swipeable": "^7.0.0",
|
||||
"react-zoom-pan-pinch": "^2.1.3",
|
||||
"string-natural-compare": "^3.0.1",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
|
@ -67,5 +69,11 @@
|
|||
"tailwindcss": "^3.1.8",
|
||||
"ts-unused-exports": "^8.0.0",
|
||||
"typescript": "^4.8.3"
|
||||
},
|
||||
"overrides": {
|
||||
"react-zoom-pan-pinch": {
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 167 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 2.6 KiB |
|
@ -246,13 +246,12 @@ export const AppLayout = ({
|
|||
<div
|
||||
id={Ids.SubPanel}
|
||||
className={cJoin(
|
||||
`texture-paper-dots overflow-y-scroll border-r-[1px] border-dark/50 bg-light
|
||||
transition-transform duration-300 [scrollbar-width:none]
|
||||
`texture-paper-dots z-20 overflow-y-scroll border-r-[1px] border-dark/50
|
||||
bg-light transition-transform duration-300 [scrollbar-width:none]
|
||||
webkit-scrollbar:w-0`,
|
||||
cIf(
|
||||
is1ColumnLayout,
|
||||
`z-10 justify-self-end border-r-0
|
||||
[grid-area:content]`,
|
||||
"justify-self-end border-r-0 [grid-area:content]",
|
||||
"[grid-area:sub]"
|
||||
),
|
||||
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)] border-l-[1px]"),
|
||||
|
@ -266,9 +265,9 @@ export const AppLayout = ({
|
|||
{/* Main panel */}
|
||||
<div
|
||||
className={cJoin(
|
||||
`texture-paper-dots overflow-y-scroll border-r-[1px] border-dark/50 bg-light
|
||||
transition-transform duration-300 [scrollbar-width:none] webkit-scrollbar:w-0`,
|
||||
cIf(is1ColumnLayout, "z-10 justify-self-start [grid-area:content]", "[grid-area:main]"),
|
||||
`texture-paper-dots z-30 overflow-y-scroll border-r-[1px] border-dark/50
|
||||
bg-light transition-transform duration-300 [scrollbar-width:none] webkit-scrollbar:w-0`,
|
||||
cIf(is1ColumnLayout, "justify-self-start [grid-area:content]", "[grid-area:main]"),
|
||||
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)]"),
|
||||
cIf(!mainPanelOpen && is1ColumnLayout, "-translate-x-full")
|
||||
)}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DetailedHTMLProps, ImgHTMLAttributes } from "react";
|
||||
import { UploadImageFragment } from "graphql/generated";
|
||||
import { getAssetURL, ImageQuality } from "helpers/img";
|
||||
import { getAssetURL, getImgSizesByQuality, ImageQuality } from "helpers/img";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -11,18 +11,36 @@ interface Props
|
|||
extends Omit<DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "src"> {
|
||||
src: UploadImageFragment | string;
|
||||
quality?: ImageQuality;
|
||||
sizeMultiplicator?: number;
|
||||
}
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const Img = ({
|
||||
className,
|
||||
src: rawSrc,
|
||||
src: propsSrc,
|
||||
quality = ImageQuality.Small,
|
||||
alt,
|
||||
loading = "lazy",
|
||||
height,
|
||||
width,
|
||||
...otherProps
|
||||
}: Props): JSX.Element => {
|
||||
const src = typeof rawSrc === "string" ? rawSrc : getAssetURL(rawSrc.url, quality);
|
||||
return <img className={className} src={src} alt={alt} loading={loading} {...otherProps} />;
|
||||
const src = typeof propsSrc === "string" ? propsSrc : getAssetURL(propsSrc.url, quality);
|
||||
const size =
|
||||
typeof propsSrc === "string"
|
||||
? { width, height }
|
||||
: getImgSizesByQuality(propsSrc.width ?? 0, propsSrc.height ?? 0, quality);
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
src={src}
|
||||
alt={alt}
|
||||
loading={loading}
|
||||
height={size.height}
|
||||
width={size.width}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ export const ContentPanel = ({
|
|||
<div className="grid h-full">
|
||||
<main
|
||||
className={cJoin(
|
||||
"justify-self-center px-4 pt-10 pb-20",
|
||||
"justify-self-center px-4 pt-10 pb-20 relative",
|
||||
cIf(isContentPanelAtLeast3xl, "px-10 pt-20 pb-32"),
|
||||
width === ContentPanelWidthSizes.Default
|
||||
? "max-w-2xl"
|
||||
|
|
|
@ -47,6 +47,34 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
}
|
||||
}
|
||||
cover {
|
||||
front {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
spine {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
back {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
front {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
full {
|
||||
data {
|
||||
attributes {
|
||||
|
@ -61,6 +89,20 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
}
|
||||
}
|
||||
}
|
||||
inside_front {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
inside_back {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dust_jacket {
|
||||
full {
|
||||
|
@ -108,6 +150,9 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
price {
|
||||
...pricePicker
|
||||
}
|
||||
size {
|
||||
width
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
|
@ -120,6 +165,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
metadata {
|
||||
__typename
|
||||
... on ComponentMetadataBooks {
|
||||
page_order
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
|
|
|
@ -180,6 +180,17 @@ query localDataGetWebsiteInterfaces {
|
|||
switch_to_grid_view
|
||||
switch_to_folder_view
|
||||
content_is_not_available
|
||||
paper_texture
|
||||
book_fold
|
||||
lighting
|
||||
side_pages
|
||||
shadow
|
||||
night_reader
|
||||
single_page_view
|
||||
double_page_view
|
||||
reset_all_options
|
||||
reading_layout
|
||||
quality
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,10 +58,10 @@ const getImgSizesByMaxSize = (
|
|||
): { width: number; height: number } => {
|
||||
if (width > height) {
|
||||
if (width < maxSize) return { width: width, height: height };
|
||||
return { width: maxSize, height: (height / width) * maxSize };
|
||||
return { width: maxSize, height: Math.ceil((height / width) * maxSize) };
|
||||
}
|
||||
if (height < maxSize) return { width: width, height: height };
|
||||
return { width: (width / height) * maxSize, height: maxSize };
|
||||
return { width: Math.ceil((width / height) * maxSize), height: maxSize };
|
||||
};
|
||||
|
||||
export const getImgSizesByQuality = (
|
||||
|
|
|
@ -20,3 +20,6 @@ export const randomInt = (min: number, max: number): number =>
|
|||
Math.floor(Math.random() * (max - min)) + min;
|
||||
|
||||
export const isInteger = (value: string): boolean => /^[+-]?[0-9]+$/u.test(value);
|
||||
|
||||
export const clamp = (value: number, min: number, max: number): number =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useIsClient } from "usehooks-ts";
|
||||
import { isDefined } from "helpers/others";
|
||||
|
||||
export const useFullscreen = (
|
||||
id: string
|
||||
): { isFullscreen: boolean; toggleFullscreen: () => void } => {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const isClient = useIsClient();
|
||||
|
||||
const elem = useMemo(() => (isClient ? document.querySelector(`#${id}`) : null), [id, isClient]);
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
if (elem) {
|
||||
if (isFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
elem.requestFullscreen();
|
||||
}
|
||||
}
|
||||
}, [elem, isFullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
const onFullscreenChanged = () => {
|
||||
setIsFullscreen(isDefined(document.fullscreenElement));
|
||||
};
|
||||
addEventListener("fullscreenchange", onFullscreenChanged);
|
||||
return () => removeEventListener("fullscreenchange", onFullscreenChanged);
|
||||
}, []);
|
||||
|
||||
return { isFullscreen, toggleFullscreen };
|
||||
};
|
|
@ -510,7 +510,9 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
|||
<div id={intersectionIds[4]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="-mb-6 text-2xl">{langui.contents}</h2>
|
||||
{displayOpenScans && (
|
||||
<Button href={`/library/${item.slug}/scans`} text={langui.view_scans} />
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
<Button href={`/library/${item.slug}/reader`} text={langui.view_scans} />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w- grid w-full gap-4">
|
||||
{filterHasAttributes(item.contents.data, ["attributes"] as const).map(
|
||||
|
@ -735,7 +737,10 @@ const ContentLine = ({
|
|||
{hasScanSet || isDefined(content) ? (
|
||||
<>
|
||||
{hasScanSet && (
|
||||
<Button href={`/library/${parentSlug}/scans#${slug}`} text={langui.view_scans} />
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
text={langui.view_scans}
|
||||
/>
|
||||
)}
|
||||
{isDefined(content) && (
|
||||
<Button href={`/contents/${content.slug}`} text={langui.open_content} />
|
||||
|
@ -787,7 +792,10 @@ const ContentLine = ({
|
|||
{hasScanSet || isDefined(content) ? (
|
||||
<>
|
||||
{hasScanSet && (
|
||||
<Button href={`/library/${parentSlug}/scans#${slug}`} text={langui.view_scans} />
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
text={langui.view_scans}
|
||||
/>
|
||||
)}
|
||||
{isDefined(content) && (
|
||||
<Button href={`/contents/${content.slug}`} text={langui.open_content} />
|
||||
|
|
|
@ -1,636 +0,0 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { Fragment, useCallback, useMemo } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { ReturnButton } from "components/PanelComponents/ReturnButton";
|
||||
import { ContentPanel, ContentPanelWidthSizes } from "components/Panels/ContentPanel";
|
||||
import { SubPanel } from "components/Panels/SubPanel";
|
||||
import { GetLibraryItemScansQuery, UploadImageFragment } from "graphql/generated";
|
||||
import { getReadySdk } from "graphql/sdk";
|
||||
import { prettyInlineTitle, prettySlug, prettyItemSubType } from "helpers/formatters";
|
||||
import {
|
||||
filterHasAttributes,
|
||||
getStatusDescription,
|
||||
isDefined,
|
||||
isDefinedAndNotEmpty,
|
||||
sortRangedContent,
|
||||
} from "helpers/others";
|
||||
import { useLightBox } from "hooks/useLightBox";
|
||||
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
||||
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
|
||||
import { PreviewCard } from "components/PreviewCard";
|
||||
import { HorizontalLine } from "components/HorizontalLine";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
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 { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img";
|
||||
import { isInteger } from "helpers/numbers";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { TranslatedProps } from "types/TranslatedProps";
|
||||
import { TranslatedNavOption } from "components/PanelComponents/NavOption";
|
||||
import { useIntersectionList } from "hooks/useIntersectionList";
|
||||
import { useIs1ColumnLayout, useIsContentPanelNoMoreThan } from "hooks/useContainerQuery";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { useAppLayout } from "contexts/AppLayoutContext";
|
||||
import { getLangui } from "graphql/fetchLocalData";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface Props extends AppLayoutRequired {
|
||||
item: NonNullable<
|
||||
NonNullable<GetLibraryItemScansQuery["libraryItems"]>["data"][number]["attributes"]
|
||||
>;
|
||||
itemId: NonNullable<NonNullable<GetLibraryItemScansQuery["libraryItems"]>["data"][number]["id"]>;
|
||||
}
|
||||
|
||||
const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
||||
const [openLightBox, LightBox] = useLightBox();
|
||||
const is1ColumnLayout = useIs1ColumnLayout();
|
||||
const { langui } = useAppLayout();
|
||||
|
||||
const ids = useMemo(
|
||||
() =>
|
||||
filterHasAttributes(item.contents?.data, ["attributes.slug"] as const).map(
|
||||
(content) => content.attributes.slug
|
||||
),
|
||||
[item.contents?.data]
|
||||
);
|
||||
const currentIntersection = useIntersectionList(ids);
|
||||
|
||||
const subPanel = useMemo(
|
||||
() => (
|
||||
<SubPanel>
|
||||
<ReturnButton
|
||||
href={`/library/${item.slug}`}
|
||||
title={langui.item}
|
||||
className="mb-4"
|
||||
displayOnlyOn="3ColumnsLayout"
|
||||
/>
|
||||
|
||||
<div className="grid place-items-center">
|
||||
<div className={cIf(is1ColumnLayout, "w-3/4")}>
|
||||
<PreviewCard
|
||||
href={`/library/${item.slug}`}
|
||||
title={item.title}
|
||||
subtitle={item.subtitle}
|
||||
thumbnail={item.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="21/29.7"
|
||||
thumbnailRounded={false}
|
||||
topChips={
|
||||
item.metadata && item.metadata.length > 0 && item.metadata[0]
|
||||
? [prettyItemSubType(item.metadata[0])]
|
||||
: []
|
||||
}
|
||||
bottomChips={filterHasAttributes(item.categories?.data, ["attributes"] as const).map(
|
||||
(category) => category.attributes.short
|
||||
)}
|
||||
metadata={{
|
||||
releaseDate: item.release_date,
|
||||
price: item.price,
|
||||
position: "Bottom",
|
||||
}}
|
||||
infoAppend={
|
||||
!isUntangibleGroupItem(item.metadata?.[0]) && <PreviewCardCTAs id={itemId} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HorizontalLine />
|
||||
|
||||
<p className="mb-4 font-headers text-2xl font-bold">{langui.contents}</p>
|
||||
|
||||
{filterHasAttributes(item.contents?.data, ["attributes"] as const).map((content, index) => (
|
||||
<>
|
||||
{content.attributes.scan_set && content.attributes.scan_set.length > 0 && (
|
||||
<TranslatedNavOption
|
||||
key={content.id}
|
||||
url={`#${content.attributes.slug}`}
|
||||
translations={filterHasAttributes(
|
||||
content.attributes.content?.data?.attributes?.translations,
|
||||
["language.data.attributes"] as const
|
||||
).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
translation.subtitle
|
||||
),
|
||||
subtitle:
|
||||
content.attributes.range[0]?.__typename === "ComponentRangePageRange"
|
||||
? `${content.attributes.range[0].starting_page}` +
|
||||
`→` +
|
||||
`${content.attributes.range[0].ending_page}`
|
||||
: undefined,
|
||||
}))}
|
||||
fallback={{
|
||||
title: prettySlug(content.attributes.slug, item.slug),
|
||||
subtitle:
|
||||
content.attributes.range[0]?.__typename === "ComponentRangePageRange"
|
||||
? `${content.attributes.range[0].starting_page}` +
|
||||
`→` +
|
||||
`${content.attributes.range[0].ending_page}`
|
||||
: undefined,
|
||||
}}
|
||||
border
|
||||
active={index === currentIntersection}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</SubPanel>
|
||||
),
|
||||
[
|
||||
currentIntersection,
|
||||
item.categories?.data,
|
||||
item.contents?.data,
|
||||
item.metadata,
|
||||
item.price,
|
||||
item.release_date,
|
||||
item.slug,
|
||||
item.subtitle,
|
||||
item.thumbnail?.data?.attributes,
|
||||
item.title,
|
||||
itemId,
|
||||
langui,
|
||||
is1ColumnLayout,
|
||||
]
|
||||
);
|
||||
|
||||
const contentPanel = useMemo(
|
||||
() => (
|
||||
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
||||
<LightBox />
|
||||
|
||||
<ReturnButton
|
||||
href={`/library/${item.slug}`}
|
||||
title={langui.item}
|
||||
displayOnlyOn="1ColumnLayout"
|
||||
className="mb-10"
|
||||
/>
|
||||
|
||||
{item.images && <ScanSetCover images={item.images} openLightBox={openLightBox} />}
|
||||
|
||||
{item.contents?.data.map((content) => (
|
||||
<Fragment key={content.id}>
|
||||
{content.attributes?.scan_set?.[0] && (
|
||||
<TranslatedScanSet
|
||||
scanSet={content.attributes.scan_set}
|
||||
openLightBox={openLightBox}
|
||||
id={content.attributes.slug}
|
||||
translations={filterHasAttributes(
|
||||
content.attributes.content?.data?.attributes?.translations,
|
||||
["language.data.attributes"] as const
|
||||
).map((translation) => ({
|
||||
language: translation.language.data.attributes.code,
|
||||
title: prettyInlineTitle(
|
||||
translation.pre_title,
|
||||
translation.title,
|
||||
translation.subtitle
|
||||
),
|
||||
}))}
|
||||
fallback={{
|
||||
title: prettySlug(content.attributes.slug, item.slug),
|
||||
}}
|
||||
content={content.attributes.content}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</ContentPanel>
|
||||
),
|
||||
[LightBox, openLightBox, item.contents?.data, item.images, item.slug, langui]
|
||||
);
|
||||
|
||||
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
|
||||
};
|
||||
export default LibrarySlug;
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ NEXT DATA FETCHING ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const langui = getLangui(context.locale);
|
||||
const item = await sdk.getLibraryItemScans({
|
||||
slug: context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "",
|
||||
language_code: context.locale ?? "en",
|
||||
});
|
||||
if (!item.libraryItems?.data[0]?.attributes || !item.libraryItems.data[0]?.id)
|
||||
return { notFound: true };
|
||||
sortRangedContent(item.libraryItems.data[0].attributes.contents);
|
||||
|
||||
const props: Props = {
|
||||
item: item.libraryItems.data[0].attributes,
|
||||
itemId: item.libraryItems.data[0].id,
|
||||
openGraph: getOpenGraph(
|
||||
langui,
|
||||
item.libraryItems.data[0].attributes.title,
|
||||
undefined,
|
||||
item.libraryItems.data[0].attributes.thumbnail?.data?.attributes
|
||||
),
|
||||
};
|
||||
return {
|
||||
props: props,
|
||||
};
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async (context) => {
|
||||
const sdk = getReadySdk();
|
||||
const libraryItems = await sdk.getLibraryItemsSlugs({});
|
||||
const paths: GetStaticPathsResult["paths"] = [];
|
||||
filterHasAttributes(libraryItems.libraryItems?.data, ["attributes"] as const).map((item) => {
|
||||
context.locales?.map((local) =>
|
||||
paths.push({ params: { slug: item.attributes.slug }, locale: local })
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
* ───────────────────────────────────────╯ COMPONENT ╰───────────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface ScanSetProps {
|
||||
openLightBox: (images: string[], index?: number) => void;
|
||||
scanSet: NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<GetLibraryItemScansQuery["libraryItems"]>["data"][number]["attributes"]
|
||||
>["contents"]
|
||||
>["data"][number]["attributes"]
|
||||
>["scan_set"]
|
||||
>;
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
content: NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<GetLibraryItemScansQuery["libraryItems"]>["data"][number]["attributes"]
|
||||
>["contents"]
|
||||
>["data"][number]["attributes"]
|
||||
>["content"];
|
||||
}
|
||||
|
||||
const ScanSet = ({ openLightBox, scanSet, id, title, content }: ScanSetProps): JSX.Element => {
|
||||
const is1ColumnLayout = useIsContentPanelNoMoreThan("2xl");
|
||||
const { langui } = useAppLayout();
|
||||
const [selectedScan, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
||||
items: scanSet,
|
||||
languageExtractor: useCallback(
|
||||
(item: NonNullable<ScanSetProps["scanSet"][number]>) => item.language?.data?.attributes?.code,
|
||||
[]
|
||||
),
|
||||
transform: useCallback((item: NonNullable<ScanSetProps["scanSet"][number]>) => {
|
||||
item.pages?.data.sort((a, b) => {
|
||||
if (
|
||||
a.attributes &&
|
||||
b.attributes &&
|
||||
isDefinedAndNotEmpty(a.attributes.url) &&
|
||||
isDefinedAndNotEmpty(b.attributes.url)
|
||||
) {
|
||||
let aName = getAssetFilename(a.attributes.url);
|
||||
let bName = getAssetFilename(b.attributes.url);
|
||||
|
||||
/*
|
||||
* If the number is a succession of 0s, make the number
|
||||
* incrementally smaller than 0 (i.e: 00 becomes -1)
|
||||
*/
|
||||
if (aName.replaceAll("0", "").length === 0) {
|
||||
aName = (1 - aName.length).toString(10);
|
||||
}
|
||||
if (bName.replaceAll("0", "").length === 0) {
|
||||
bName = (1 - bName.length).toString(10);
|
||||
}
|
||||
|
||||
if (isInteger(aName) && isInteger(bName)) {
|
||||
return parseInt(aName, 10) - parseInt(bName, 10);
|
||||
}
|
||||
return a.attributes.url.localeCompare(b.attributes.url);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return item;
|
||||
}, []),
|
||||
});
|
||||
|
||||
const pages = useMemo(
|
||||
() => filterHasAttributes(selectedScan?.pages?.data, ["attributes"]),
|
||||
[selectedScan]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedScan && isDefined(pages) && (
|
||||
<div>
|
||||
<div
|
||||
className="flex flex-row flex-wrap place-items-center
|
||||
gap-6 pt-10 text-base first-of-type:pt-0">
|
||||
<h2 id={id} className="text-2xl">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<Chip
|
||||
text={
|
||||
selectedScan.language?.data?.attributes?.code ===
|
||||
selectedScan.source_language?.data?.attributes?.code
|
||||
? langui.scan ?? "Scan"
|
||||
: langui.scanlation ?? "Scanlation"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-wrap place-items-center gap-4 pb-6">
|
||||
{content?.data?.attributes && isDefinedAndNotEmpty(content.data.attributes.slug) && (
|
||||
<Button
|
||||
href={`/contents/${content.data.attributes.slug}`}
|
||||
text={langui.open_content}
|
||||
/>
|
||||
)}
|
||||
|
||||
{languageSwitcherProps.locales.size > 1 && (
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
)}
|
||||
|
||||
<div className="grid place-content-center place-items-center">
|
||||
<p className="font-headers font-bold">{langui.status}:</p>
|
||||
<ToolTip
|
||||
content={getStatusDescription(selectedScan.status, langui)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedScan.status} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
{selectedScan.scanners && selectedScan.scanners.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.scanners}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedScan.scanners.data, [
|
||||
"id",
|
||||
"attributes",
|
||||
] as const).map((scanner) => (
|
||||
<Fragment key={scanner.id}>
|
||||
<RecorderChip recorder={scanner.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.cleaners}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedScan.cleaners.data, [
|
||||
"id",
|
||||
"attributes",
|
||||
] as const).map((cleaner) => (
|
||||
<Fragment key={cleaner.id}>
|
||||
<RecorderChip recorder={cleaner.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedScan.typesetters && selectedScan.typesetters.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.typesetters}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedScan.typesetters.data, [
|
||||
"id",
|
||||
"attributes",
|
||||
] as const).map((typesetter) => (
|
||||
<Fragment key={typesetter.id}>
|
||||
<RecorderChip recorder={typesetter.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDefinedAndNotEmpty(selectedScan.notes) && (
|
||||
<ToolTip content={selectedScan.notes}>
|
||||
<Chip text={langui.notes ?? "Notes"} />
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cJoin(
|
||||
`grid items-end gap-8 border-b-[3px] border-dotted pb-12
|
||||
last-of-type:border-0`,
|
||||
cIf(
|
||||
is1ColumnLayout,
|
||||
"grid-cols-2 gap-[4vmin]",
|
||||
"grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))]"
|
||||
)
|
||||
)}>
|
||||
{pages.map((page, index) => (
|
||||
<div
|
||||
key={page.id}
|
||||
className="cursor-pointer transition-transform
|
||||
drop-shadow-shade-lg hover:scale-[1.02]"
|
||||
onClick={() => {
|
||||
const images = pages.map((image) =>
|
||||
getAssetURL(image.attributes.url, ImageQuality.Large)
|
||||
);
|
||||
openLightBox(images, index);
|
||||
}}>
|
||||
<Img src={page.attributes} quality={ImageQuality.Small} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
const TranslatedScanSet = ({
|
||||
translations,
|
||||
fallback,
|
||||
...otherProps
|
||||
}: TranslatedProps<ScanSetProps, "title">): JSX.Element => {
|
||||
const [selectedTranslation] = useSmartLanguage({
|
||||
items: translations,
|
||||
languageExtractor: useCallback((item: { language: string }): string => item.language, []),
|
||||
});
|
||||
|
||||
return <ScanSet title={selectedTranslation?.title ?? fallback.title} {...otherProps} />;
|
||||
};
|
||||
|
||||
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
|
||||
interface ScanSetCoverProps {
|
||||
openLightBox: (images: string[], index?: number) => void;
|
||||
images: NonNullable<
|
||||
NonNullable<
|
||||
NonNullable<GetLibraryItemScansQuery["libraryItems"]>["data"][number]["attributes"]
|
||||
>["images"]
|
||||
>;
|
||||
}
|
||||
|
||||
const ScanSetCover = ({ openLightBox, images }: ScanSetCoverProps): JSX.Element => {
|
||||
const is1ColumnLayout = useIsContentPanelNoMoreThan("4xl");
|
||||
const { langui } = useAppLayout();
|
||||
const [selectedScan, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
||||
items: images,
|
||||
languageExtractor: useCallback(
|
||||
(item: NonNullable<ScanSetCoverProps["images"][number]>) =>
|
||||
item.language?.data?.attributes?.code,
|
||||
[]
|
||||
),
|
||||
});
|
||||
|
||||
const coverImages = useMemo(() => {
|
||||
const memo: UploadImageFragment[] = [];
|
||||
if (selectedScan?.obi_belt?.full?.data?.attributes)
|
||||
memo.push(selectedScan.obi_belt.full.data.attributes);
|
||||
if (selectedScan?.obi_belt?.inside_full?.data?.attributes)
|
||||
memo.push(selectedScan.obi_belt.inside_full.data.attributes);
|
||||
if (selectedScan?.dust_jacket?.full?.data?.attributes)
|
||||
memo.push(selectedScan.dust_jacket.full.data.attributes);
|
||||
if (selectedScan?.dust_jacket?.inside_full?.data?.attributes)
|
||||
memo.push(selectedScan.dust_jacket.inside_full.data.attributes);
|
||||
if (selectedScan?.cover?.full?.data?.attributes)
|
||||
memo.push(selectedScan.cover.full.data.attributes);
|
||||
if (selectedScan?.cover?.inside_full?.data?.attributes)
|
||||
memo.push(selectedScan.cover.inside_full.data.attributes);
|
||||
return memo;
|
||||
}, [selectedScan]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{coverImages.length > 0 && selectedScan && (
|
||||
<div>
|
||||
<div
|
||||
className="flex flex-row flex-wrap place-items-center
|
||||
gap-6 pt-10 text-base first-of-type:pt-0">
|
||||
<h2 id={"cover"} className="text-2xl">
|
||||
{langui.cover}
|
||||
</h2>
|
||||
|
||||
<Chip
|
||||
text={
|
||||
selectedScan.language?.data?.attributes?.code ===
|
||||
selectedScan.source_language?.data?.attributes?.code
|
||||
? langui.scan ?? "Scan"
|
||||
: langui.scanlation ?? "Scanlation"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-wrap place-items-center gap-4 pb-6">
|
||||
<LanguageSwitcher {...languageSwitcherProps} />
|
||||
|
||||
<div className="grid place-content-center place-items-center">
|
||||
<p className="font-headers font-bold">{langui.status}:</p>
|
||||
<ToolTip
|
||||
content={getStatusDescription(selectedScan.status, langui)}
|
||||
maxWidth={"20rem"}>
|
||||
<Chip text={selectedScan.status} />
|
||||
</ToolTip>
|
||||
</div>
|
||||
|
||||
{selectedScan.scanners && selectedScan.scanners.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.scanners}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedScan.scanners.data, [
|
||||
"id",
|
||||
"attributes",
|
||||
] as const).map((scanner) => (
|
||||
<Fragment key={scanner.id}>
|
||||
<RecorderChip recorder={scanner.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.cleaners}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedScan.cleaners.data, [
|
||||
"id",
|
||||
"attributes",
|
||||
] as const).map((cleaner) => (
|
||||
<Fragment key={cleaner.id}>
|
||||
<RecorderChip recorder={cleaner.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedScan.typesetters && selectedScan.typesetters.data.length > 0 && (
|
||||
<div>
|
||||
<p className="font-headers font-bold">{langui.typesetters}:</p>
|
||||
<div className="grid place-content-center place-items-center gap-2">
|
||||
{filterHasAttributes(selectedScan.typesetters.data, [
|
||||
"id",
|
||||
"attributes",
|
||||
] as const).map((typesetter) => (
|
||||
<Fragment key={typesetter.id}>
|
||||
<RecorderChip recorder={typesetter.attributes} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cJoin(
|
||||
`grid items-end gap-8 border-b-[3px] border-dotted pb-12
|
||||
last-of-type:border-0`,
|
||||
cIf(
|
||||
is1ColumnLayout,
|
||||
"grid-cols-2 gap-[4vmin]",
|
||||
"grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))]"
|
||||
)
|
||||
)}>
|
||||
{coverImages.map((image, index) => (
|
||||
<div
|
||||
key={image.url}
|
||||
className="cursor-pointer transition-transform
|
||||
drop-shadow-shade-lg hover:scale-[1.02]"
|
||||
onClick={() => {
|
||||
const imgs = coverImages.map((img) => getAssetURL(img.url, ImageQuality.Large));
|
||||
|
||||
openLightBox(imgs, index);
|
||||
}}>
|
||||
<Img src={image} quality={ImageQuality.Small} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
223
src/tailwind.css
|
@ -285,3 +285,226 @@ input[type="submit"] {
|
|||
.tippy-content {
|
||||
@apply relative z-10 px-6 py-4;
|
||||
}
|
||||
|
||||
/* RC SLIDER */
|
||||
|
||||
.rc-slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
touch-action: none;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.rc-slider * {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.rc-slider-rail {
|
||||
@apply h-2 rounded-full bg-mid/80;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
.rc-slider-track {
|
||||
@apply h-2 rounded-full bg-mid bg-dark/20 shadow-inner-sm shadow-shade;
|
||||
position: absolute;
|
||||
}
|
||||
.rc-slider-handle {
|
||||
@apply h-4 w-4 -mt-1 rounded-full bg-dark transition-shadow;
|
||||
position: absolute;
|
||||
cursor: grab;
|
||||
touch-action: pan-x;
|
||||
}
|
||||
.rc-slider-handle-dragging.rc-slider-handle-dragging.rc-slider-handle-dragging {
|
||||
@apply shadow-sm shadow-shade;
|
||||
}
|
||||
|
||||
.rc-slider-mark {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
.rc-slider-mark-text {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rc-slider-mark-text-active {
|
||||
color: #666;
|
||||
}
|
||||
.rc-slider-step {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
.rc-slider-dot {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
vertical-align: middle;
|
||||
background-color: #fff;
|
||||
border: 2px solid #e9e9e9;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rc-slider-dot-active {
|
||||
border-color: #96dbfa;
|
||||
}
|
||||
.rc-slider-dot-reverse {
|
||||
margin-right: -4px;
|
||||
}
|
||||
.rc-slider-disabled {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
.rc-slider-disabled .rc-slider-track {
|
||||
background-color: #ccc;
|
||||
}
|
||||
.rc-slider-disabled .rc-slider-handle,
|
||||
.rc-slider-disabled .rc-slider-dot {
|
||||
background-color: #fff;
|
||||
border-color: #ccc;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.rc-slider-disabled .rc-slider-mark-text,
|
||||
.rc-slider-disabled .rc-slider-dot {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
.rc-slider-vertical {
|
||||
width: 14px;
|
||||
height: 100%;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.rc-slider-vertical .rc-slider-rail {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
.rc-slider-vertical .rc-slider-track {
|
||||
bottom: 0;
|
||||
left: 5px;
|
||||
width: 4px;
|
||||
}
|
||||
.rc-slider-vertical .rc-slider-handle {
|
||||
margin-top: 0;
|
||||
margin-left: -5px;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
.rc-slider-vertical .rc-slider-mark {
|
||||
top: 0;
|
||||
left: 18px;
|
||||
height: 100%;
|
||||
}
|
||||
.rc-slider-vertical .rc-slider-step {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
.rc-slider-vertical .rc-slider-dot {
|
||||
margin-left: -2px;
|
||||
}
|
||||
.rc-slider-tooltip-zoom-down-enter,
|
||||
.rc-slider-tooltip-zoom-down-appear {
|
||||
display: block !important;
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.rc-slider-tooltip-zoom-down-leave {
|
||||
display: block !important;
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.rc-slider-tooltip-zoom-down-enter.rc-slider-tooltip-zoom-down-enter-active,
|
||||
.rc-slider-tooltip-zoom-down-appear.rc-slider-tooltip-zoom-down-appear-active {
|
||||
animation-name: rcSliderTooltipZoomDownIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
.rc-slider-tooltip-zoom-down-leave.rc-slider-tooltip-zoom-down-leave-active {
|
||||
animation-name: rcSliderTooltipZoomDownOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
.rc-slider-tooltip-zoom-down-enter,
|
||||
.rc-slider-tooltip-zoom-down-appear {
|
||||
transform: scale(0, 0);
|
||||
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
.rc-slider-tooltip-zoom-down-leave {
|
||||
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
||||
}
|
||||
@keyframes rcSliderTooltipZoomDownIn {
|
||||
0% {
|
||||
transform: scale(0, 0);
|
||||
transform-origin: 50% 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1, 1);
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
}
|
||||
@keyframes rcSliderTooltipZoomDownOut {
|
||||
0% {
|
||||
transform: scale(1, 1);
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0, 0);
|
||||
transform-origin: 50% 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.rc-slider-tooltip {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
visibility: visible;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.rc-slider-tooltip * {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.rc-slider-tooltip-hidden {
|
||||
display: none;
|
||||
}
|
||||
.rc-slider-tooltip-placement-top {
|
||||
padding: 4px 0 8px 0;
|
||||
}
|
||||
.rc-slider-tooltip-inner {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
padding: 6px 2px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
background-color: #6c6c6c;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 4px #d9d9d9;
|
||||
}
|
||||
.rc-slider-tooltip-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
.rc-slider-tooltip-placement-top .rc-slider-tooltip-arrow {
|
||||
bottom: 4px;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
border-width: 4px 4px 0;
|
||||
border-top-color: #6c6c6c;
|
||||
}
|
||||
|
|