Add download button for scans archives
This commit is contained in:
parent
5d2fe252ec
commit
f8f98ec41e
|
@ -0,0 +1,45 @@
|
|||
# /!\ For URLs, don't include the traling '/'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
# │ PRIVATE VARIABLES │
|
||||
# └─────────────────────┘
|
||||
|
||||
## STRAPI
|
||||
|
||||
URL_GRAPHQL=https://url-to.strapi-accords-library.com/graphql
|
||||
ACCESS_TOKEN=abcdef0123456789
|
||||
REVALIDATION_TOKEN=abcdef0123456789
|
||||
|
||||
## MAILING
|
||||
|
||||
SMTP_HOST=email.provider.com
|
||||
SMTP_USER=email@example.com
|
||||
SMTP_PASSWORD=mypassword123
|
||||
|
||||
|
||||
|
||||
# ┌────────────────────┐
|
||||
# │ PUBLIC VARIABLES │
|
||||
# └────────────────────┘
|
||||
|
||||
## ASSETS
|
||||
|
||||
NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com
|
||||
NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com
|
||||
NEXT_PUBLIC_URL_WATCH=https://url-to.watch-accords-library.com
|
||||
NEXT_PUBLIC_URL_SELF=https://url-to-front-accords-library.com
|
||||
NEXT_PUBLIC_URL_SCANS_DOWNLOAD=https://url-to.search-accords-library.com
|
||||
|
||||
## MEILISEARCH
|
||||
|
||||
NEXT_PUBLIC_URL_MEILISEARCH=https://url-to.search-accords-library.com
|
||||
NEXT_PUBLIC_MEILISEARCH_KEY=abcdef0123456789
|
||||
|
||||
## UMAMI
|
||||
|
||||
NEXT_PUBLIC_UMAMI_URL=https://url-to.umami-accords-library.com
|
||||
NEXT_PUBLIC_UMAMI_ID=abcdef0123456789
|
||||
|
||||
## OCR.SPACE
|
||||
|
||||
NEXT_PUBLIC_API_OCR_KEY=abcdef0123456789
|
26
README.md
26
README.md
|
@ -134,10 +134,11 @@ A detailled look at the technologies used in this repository:
|
|||
- [ts-unused-exports](https://www.npmjs.com/package/ts-unused-exports) to find unused exported functions/constants...
|
||||
|
||||
- Other
|
||||
|
||||
- Custom book reader based on [Okuma-Reader](https://github.com/DrMint/Okuma-Reader)
|
||||
- Support for [Material Symbols](https://fonts.google.com/icons)
|
||||
- Custom lightbox using [react-zoom-pan-pinch](https://www.npmjs.com/package/react-zoom-pan-pinch)
|
||||
- Handle query params using [Zod](https://zod.dev/)
|
||||
- Handle query params type-validation using [Zod](https://zod.dev/)
|
||||
- A secret "Terminal" mode. Can you find it?
|
||||
|
||||
## Installation
|
||||
|
@ -148,31 +149,14 @@ cd accords-library.com
|
|||
npm install
|
||||
```
|
||||
|
||||
Create a env file:
|
||||
Create a env file based on the example one:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
nano .env.local
|
||||
```
|
||||
|
||||
Enter the following information:
|
||||
|
||||
```
|
||||
URL_GRAPHQL=https://url-to.strapi-accords-library.com/graphql
|
||||
ACCESS_TOKEN=abcdef0123456789
|
||||
REVALIDATION_TOKEN=abcdef0123456789
|
||||
SMTP_HOST=email.provider.com
|
||||
SMTP_USER=email@example.com
|
||||
SMTP_PASSWORD=mypassword123
|
||||
NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com
|
||||
NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com
|
||||
NEXT_PUBLIC_URL_WATCH=https://url-to.watch-accords-library.com
|
||||
NEXT_PUBLIC_URL_SELF=https://url-to-front-accords-library.com
|
||||
NEXT_PUBLIC_URL_MEILISEARCH=https://url-to.search-accords-library.com
|
||||
NEXT_PUBLIC_MEILISEARCH_KEY=abcdef0123456789
|
||||
NEXT_PUBLIC_UMAMI_URL=https://url-to.umami-accords-library.com
|
||||
NEXT_PUBLIC_UMAMI_ID=abcdef0123456789
|
||||
NEXT_PUBLIC_API_OCR_KEY=abcdef0123456789
|
||||
```
|
||||
Change the variables
|
||||
|
||||
Run in dev mode:
|
||||
|
||||
|
|
|
@ -185,7 +185,8 @@
|
|||
"weapons_description": "A list of all the weapons across all of the games. All distinguished weapons come with an “account.” It’s a document with various details like how the weapon was forged and how it’s been used in the past.",
|
||||
"level_x": "Level {x}",
|
||||
"story_x": "Story {x}",
|
||||
"player_name_tooltip": "Certain in-game texts use the player's name as part of the dialogue/narration. If you want to see your name in the transcript found on this website, feel free to enter your player's name here. If left empty, '(player)' will be used instead."
|
||||
"player_name_tooltip": "Certain in-game texts use the player's name as part of the dialogue/narration. If you want to see your name in the transcript found on this website, feel free to enter your player's name here. If left empty, '(player)' will be used instead.",
|
||||
"download_scans": "Download scans"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -372,7 +373,8 @@
|
|||
"weapons_description": "Une liste de toutes les armes présentes dans tous les jeux. Toutes les armes distinguées sont accompagnées d'un \"compte\". Il s'agit d'un document contenant divers détails tels que la façon dont l'arme a été forgée et comment elle a été utilisée dans le passé.",
|
||||
"level_x": "Niveau {x}",
|
||||
"story_x": "Histoire {x}",
|
||||
"player_name_tooltip": "Certains textes dans les jeux utilisent le nom du joueur dans les dialogue/la narration. Si vous voulez voir votre nom dans les transcriptions se trouvant sur ce site web, n'hésitez pas à entrer votre nom de joueur ici. S'il n'est pas renseigné, '(player)' sera utilisé à la place."
|
||||
"player_name_tooltip": "Certains textes dans les jeux utilisent le nom du joueur dans les dialogue/la narration. Si vous voulez voir votre nom dans les transcriptions se trouvant sur ce site web, n'hésitez pas à entrer votre nom de joueur ici. S'il n'est pas renseigné, '(player)' sera utilisé à la place.",
|
||||
"download_scans": "Télécharger les scans"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -559,7 +561,8 @@
|
|||
"weapons_description": null,
|
||||
"level_x": null,
|
||||
"story_x": null,
|
||||
"player_name_tooltip": null
|
||||
"player_name_tooltip": null,
|
||||
"download_scans": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -746,7 +749,8 @@
|
|||
"weapons_description": null,
|
||||
"level_x": null,
|
||||
"story_x": null,
|
||||
"player_name_tooltip": null
|
||||
"player_name_tooltip": null,
|
||||
"download_scans": null
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -933,7 +937,8 @@
|
|||
"weapons_description": null,
|
||||
"level_x": null,
|
||||
"story_x": null,
|
||||
"player_name_tooltip": null
|
||||
"player_name_tooltip": null,
|
||||
"download_scans": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -204,7 +204,7 @@ export const PreviewCard = ({
|
|||
)}
|
||||
{subtitle && <Markdown text={subtitle} className="leading-none break-words" />}
|
||||
</div>
|
||||
{description && <Markdown text={description} className="break-words overflow-hidden" />}
|
||||
{description && <Markdown text={description} className="overflow-hidden break-words" />}
|
||||
{bottomChips && bottomChips.length > 0 && (
|
||||
<div
|
||||
className="grid grid-flow-col place-content-start gap-1 overflow-x-scroll
|
||||
|
|
|
@ -184,4 +184,5 @@ export interface ICUParams {
|
|||
level_x: { x: Date | boolean | number | string };
|
||||
story_x: { x: Date | boolean | number | string };
|
||||
player_name_tooltip: never;
|
||||
download_scans: never;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ query getLibraryItem($slug: String, $language_code: String) {
|
|||
root_item
|
||||
primary
|
||||
digital
|
||||
download_available
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
|
|
|
@ -6,6 +6,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
|
|||
slug
|
||||
title
|
||||
subtitle
|
||||
download_available
|
||||
images(pagination: { limit: -1 }) {
|
||||
status
|
||||
language {
|
||||
|
|
|
@ -191,6 +191,7 @@ query localDataGetWebsiteInterfaces {
|
|||
level_x
|
||||
story_x
|
||||
player_name_tooltip
|
||||
download_scans
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useMemo } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
|
@ -20,7 +21,6 @@ import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder";
|
|||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { Chip } from "components/Chip";
|
||||
import { useMemo } from "react";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
|
@ -268,8 +268,8 @@ const NoContentNorFolderMessage = () => {
|
|||
return (
|
||||
<div className="grid place-content-center">
|
||||
<div
|
||||
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
|
||||
border-dark p-8 text-dark opacity-40 mt-12">
|
||||
className="mt-12 grid grid-flow-col place-items-center gap-9 rounded-2xl border-2
|
||||
border-dotted border-dark p-8 text-dark opacity-40">
|
||||
<p className="max-w-xs text-2xl">{format("empty_folder_message")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -41,7 +41,6 @@ import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
|||
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
||||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||
import { WithLabel } from "components/Inputs/WithLabel";
|
||||
import { Ico } from "components/Ico";
|
||||
import { cJoin, cIf } from "helpers/className";
|
||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
|
@ -50,10 +49,10 @@ import { useIntersectionList } from "hooks/useIntersectionList";
|
|||
import { Ids } from "types/ids";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||
import { Link } from "components/Inputs/Link";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { ElementsSeparator } from "helpers/component";
|
||||
import { ToolTip } from "components/ToolTip";
|
||||
|
||||
/*
|
||||
* ╭─────────────╮
|
||||
|
@ -95,10 +94,13 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
|||
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
|
||||
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set";
|
||||
|
||||
const displayOpenScans = item.contents?.data.some(
|
||||
const hasContentScans = item.contents?.data.some(
|
||||
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
|
||||
);
|
||||
|
||||
const hasContentSection =
|
||||
(item.contents && item.contents.data.length > 0) || item.download_available;
|
||||
|
||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
|
||||
const subPanel = (
|
||||
|
@ -205,11 +207,12 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
|||
item.subitem_of.data[0].attributes.title,
|
||||
item.subitem_of.data[0].attributes.subtitle
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid place-items-center text-center">
|
||||
<h1 className="text-3xl">{item.title}</h1>
|
||||
<h1 className="text-4xl">{item.title}</h1>
|
||||
{isDefinedAndNotEmpty(item.subtitle) && <h2 className="text-2xl">{item.subtitle}</h2>}
|
||||
</div>
|
||||
|
||||
|
@ -517,58 +520,75 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{item.contents && item.contents.data.length > 0 && (
|
||||
{hasContentSection && (
|
||||
<div id={intersectionIds[4]} className="grid w-full place-items-center gap-8">
|
||||
<h2 className="-mb-6 text-2xl">{format("contents")}</h2>
|
||||
{displayOpenScans && (
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
<Button href={`/library/${item.slug}/reader`} text={format("view_scans")} />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w- grid w-full gap-4">
|
||||
{filterHasAttributes(item.contents.data, ["attributes"]).map((rangedContent) => (
|
||||
<ContentLine
|
||||
content={
|
||||
rangedContent.attributes.content?.data?.attributes
|
||||
? {
|
||||
translations: filterDefined(
|
||||
rangedContent.attributes.content.data.attributes.translations
|
||||
).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language?.data?.attributes?.code,
|
||||
})),
|
||||
categories: filterHasAttributes(
|
||||
rangedContent.attributes.content.data.attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => category.attributes.short),
|
||||
type:
|
||||
rangedContent.attributes.content.data.attributes.type?.data?.attributes
|
||||
?.titles?.[0]?.title ??
|
||||
prettySlug(
|
||||
rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes?.slug
|
||||
),
|
||||
slug: rangedContent.attributes.content.data.attributes.slug,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rangeStart={
|
||||
rangedContent.attributes.range[0]?.__typename === "ComponentRangePageRange"
|
||||
? `${rangedContent.attributes.range[0].starting_page}`
|
||||
: ""
|
||||
}
|
||||
slug={rangedContent.attributes.slug}
|
||||
parentSlug={item.slug}
|
||||
key={rangedContent.id}
|
||||
hasScanSet={
|
||||
isDefined(rangedContent.attributes.scan_set) &&
|
||||
rangedContent.attributes.scan_set.length > 0
|
||||
}
|
||||
condensed={!isContentPanelAtLeast3xl}
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
{hasContentScans && (
|
||||
<Button
|
||||
href={`/library/${item.slug}/reader`}
|
||||
icon="auto_stories"
|
||||
text={format("view_scans")}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
{item.download_available && (
|
||||
<Button
|
||||
href={`${process.env.NEXT_PUBLIC_URL_SCANS_DOWNLOAD}/library/scans/${item.slug}.zip`}
|
||||
icon="download"
|
||||
text={format("download_scans")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid w-full gap-4">
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid items-center",
|
||||
cIf(isContentPanelAtLeast3xl, "grid-cols-[1fr_auto_auto_auto] gap-4", "gap-4")
|
||||
)}>
|
||||
{filterHasAttributes(item.contents?.data, ["attributes"]).map((rangedContent) => (
|
||||
<ContentItem
|
||||
content={
|
||||
rangedContent.attributes.content?.data?.attributes
|
||||
? {
|
||||
translations: filterDefined(
|
||||
rangedContent.attributes.content.data.attributes.translations
|
||||
).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language?.data?.attributes?.code,
|
||||
})),
|
||||
categories: filterHasAttributes(
|
||||
rangedContent.attributes.content.data.attributes.categories?.data,
|
||||
["attributes"]
|
||||
).map((category) => category.attributes.short),
|
||||
type:
|
||||
rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes?.titles?.[0]?.title ??
|
||||
prettySlug(
|
||||
rangedContent.attributes.content.data.attributes.type?.data
|
||||
?.attributes?.slug
|
||||
),
|
||||
slug: rangedContent.attributes.content.data.attributes.slug,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rangeStart={
|
||||
rangedContent.attributes.range[0]?.__typename === "ComponentRangePageRange"
|
||||
? `${rangedContent.attributes.range[0].starting_page}`
|
||||
: ""
|
||||
}
|
||||
slug={rangedContent.attributes.slug}
|
||||
parentSlug={item.slug}
|
||||
key={rangedContent.id}
|
||||
hasScanSet={
|
||||
isDefined(rangedContent.attributes.scan_set) &&
|
||||
rangedContent.attributes.scan_set.length > 0
|
||||
}
|
||||
displayType={isContentPanelAtLeast3xl ? "row" : "card"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -647,7 +667,7 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
|
|||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
interface ContentLineProps {
|
||||
interface ContentItemProps {
|
||||
content?: {
|
||||
translations: {
|
||||
pre_title: string | null | undefined;
|
||||
|
@ -662,84 +682,32 @@ interface ContentLineProps {
|
|||
rangeStart: string;
|
||||
parentSlug: string;
|
||||
slug: string;
|
||||
|
||||
hasScanSet: boolean;
|
||||
condensed: boolean;
|
||||
displayType: "card" | "row";
|
||||
}
|
||||
|
||||
const ContentLine = ({
|
||||
const ContentItem = ({
|
||||
rangeStart,
|
||||
content,
|
||||
hasScanSet,
|
||||
slug,
|
||||
parentSlug,
|
||||
condensed,
|
||||
}: ContentLineProps): JSX.Element => {
|
||||
displayType,
|
||||
}: ContentItemProps): JSX.Element => {
|
||||
const { format } = useFormat();
|
||||
const { value: isOpened, toggle: toggleOpened } = useBoolean(false);
|
||||
const [selectedTranslation] = useSmartLanguage({
|
||||
items: content?.translations ?? [],
|
||||
languageExtractor: useCallback(
|
||||
(item: NonNullable<ContentLineProps["content"]>["translations"][number]) => item.language,
|
||||
(item: NonNullable<ContentItemProps["content"]>["translations"][number]) => item.language,
|
||||
[]
|
||||
),
|
||||
});
|
||||
|
||||
if (condensed) {
|
||||
if (displayType === "card") {
|
||||
return (
|
||||
<div className="my-4 grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
{content?.type && <Chip text={content.type} />}
|
||||
<p className="h-4 w-full border-b-2 border-dotted border-black opacity-30" />
|
||||
<p>{rangeStart}</p>
|
||||
</div>
|
||||
|
||||
<h3 className="flex flex-wrap place-items-center gap-2">
|
||||
{selectedTranslation
|
||||
? prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)
|
||||
: content
|
||||
? prettySlug(content.slug, parentSlug)
|
||||
: prettySlug(slug, parentSlug)}
|
||||
</h3>
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
{content?.categories?.map((category, index) => (
|
||||
<Chip key={index} text={category} />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{hasScanSet || isDefined(content) ? (
|
||||
<>
|
||||
{hasScanSet && (
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
text={format("view_scans")}
|
||||
/>
|
||||
)}
|
||||
{isDefined(content) && (
|
||||
<Button href={`/contents/${content.slug}`} text={format("open_content")} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
format("content_is_not_available")
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid gap-2 rounded-lg px-4",
|
||||
cIf(isOpened, "my-2 h-auto bg-mid py-3 shadow-inner-sm shadow-shade")
|
||||
)}>
|
||||
<div className="grid grid-cols-[auto_auto_1fr_auto_12ch] place-items-center gap-4">
|
||||
<Link href={""} linkStyled>
|
||||
<h3 className="cursor-pointer" onClick={toggleOpened}>
|
||||
<div className="grid w-full gap-3 rounded-xl bg-light p-8 shadow-sm shadow-shade">
|
||||
<div className="grid grid-cols-[auto_1fr_auto] items-center gap-3">
|
||||
<h3 className="text-lg">
|
||||
{selectedTranslation
|
||||
? prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
|
@ -750,38 +718,95 @@ const ContentLine = ({
|
|||
? prettySlug(content.slug, parentSlug)
|
||||
: prettySlug(slug, parentSlug)}
|
||||
</h3>
|
||||
</Link>
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
{content?.categories?.map((category, index) => (
|
||||
<Chip key={index} text={category} />
|
||||
))}
|
||||
<p className="h-4 w-full border-b-2 border-dotted border-mid" />
|
||||
<p className="text-right">{rangeStart}</p>
|
||||
</div>
|
||||
<p className="h-4 w-full border-b-2 border-dotted border-black opacity-30" />
|
||||
<p>{rangeStart}</p>
|
||||
{content?.type && <Chip className="justify-self-end" text={content.type} />}
|
||||
</div>
|
||||
<div
|
||||
className={`grid-flow-col place-content-start place-items-center gap-2 ${
|
||||
isOpened ? "grid" : "hidden"
|
||||
}`}>
|
||||
<Ico icon={"subdirectory_arrow_right"} className="text-dark" />
|
||||
|
||||
{hasScanSet || isDefined(content) ? (
|
||||
<>
|
||||
{content && (
|
||||
<div>
|
||||
{content.categories && (
|
||||
<div className="flex items-center gap-2">
|
||||
<p>{format("category", { count: content.categories.length })}</p>
|
||||
<div className="flex flex-row flex-wrap gap-1">
|
||||
{content.categories.map((category, index) => (
|
||||
<Chip key={index} text={category} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content.type && (
|
||||
<div className="flex items-center gap-2">
|
||||
<p>{format("type", { count: 1 })}</p>
|
||||
<Chip className="justify-self-end" text={content.type} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(hasScanSet || content) && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{hasScanSet && (
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
icon="auto_stories"
|
||||
text={format("view_scans")}
|
||||
/>
|
||||
)}
|
||||
{isDefined(content) && (
|
||||
<Button href={`/contents/${content.slug}`} text={format("open_content")} />
|
||||
<Button
|
||||
href={`/contents/${content.slug}`}
|
||||
icon="subject"
|
||||
text={format("open_content")}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
format("content_is_not_available")
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-[auto_auto_1fr_auto] items-center gap-3">
|
||||
<h3>
|
||||
{selectedTranslation
|
||||
? prettyInlineTitle(
|
||||
selectedTranslation.pre_title,
|
||||
selectedTranslation.title,
|
||||
selectedTranslation.subtitle
|
||||
)
|
||||
: content
|
||||
? prettySlug(content.slug, parentSlug)
|
||||
: prettySlug(slug, parentSlug)}
|
||||
</h3>
|
||||
<div className="flex flex-wrap place-content-center gap-1">
|
||||
{content?.categories?.map((category, index) => (
|
||||
<Chip key={index} text={category} />
|
||||
))}
|
||||
</div>
|
||||
<p className="h-4 w-full border-b-2 border-dotted border-mid" />
|
||||
{content?.type && <Chip className="justify-self-end" text={content.type} />}
|
||||
</div>
|
||||
<p className="text-right">{rangeStart}</p>
|
||||
<div>
|
||||
{hasScanSet && (
|
||||
<ToolTip content={format("view_scans")}>
|
||||
<Button
|
||||
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
|
||||
icon="auto_stories"
|
||||
size="small"
|
||||
/>
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{isDefined(content) && (
|
||||
<ToolTip content={format("open_content")}>
|
||||
<Button href={`/contents/${content.slug}`} icon="subject" size="small" />
|
||||
</ToolTip>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -366,6 +366,14 @@ const LibrarySlug = ({
|
|||
sendAnalytics("Reader", "Reset all options");
|
||||
}}
|
||||
/>
|
||||
|
||||
{item.download_available && (
|
||||
<Button
|
||||
href={`${process.env.NEXT_PUBLIC_URL_SCANS_DOWNLOAD}/library/scans/${item.slug}.zip`}
|
||||
icon="download"
|
||||
text={format("download_scans")}
|
||||
/>
|
||||
)}
|
||||
</SubPanel>
|
||||
);
|
||||
|
||||
|
@ -855,6 +863,7 @@ const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps):
|
|||
{content?.data?.attributes && isDefinedAndNotEmpty(content.data.attributes.slug) && (
|
||||
<Button
|
||||
href={`/contents/${content.data.attributes.slug}`}
|
||||
icon="subject"
|
||||
text={format("open_content")}
|
||||
/>
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue