Add download button for scans archives

This commit is contained in:
DrMint 2023-05-11 11:24:36 +02:00
parent 5d2fe252ec
commit f8f98ec41e
11 changed files with 238 additions and 166 deletions

45
.env.example Executable file
View File

@ -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

View File

@ -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:

View File

@ -185,7 +185,8 @@
"weapons_description": "A list of all the weapons across all of the games. All distinguished weapons come with an “account.” Its a document with various details like how the weapon was forged and how its 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
}
}
]

View File

@ -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

View File

@ -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;
}

View File

@ -9,6 +9,7 @@ query getLibraryItem($slug: String, $language_code: String) {
root_item
primary
digital
download_available
thumbnail {
data {
attributes {

View File

@ -6,6 +6,7 @@ query getLibraryItemScans($slug: String, $language_code: String) {
slug
title
subtitle
download_available
images(pagination: { limit: -1 }) {
status
language {

View File

@ -191,6 +191,7 @@ query localDataGetWebsiteInterfaces {
level_x
story_x
player_name_tooltip
download_scans
}
}
}

View File

@ -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>

View File

@ -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,17 +520,33 @@ 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>
{hasContentScans && (
<Button
href={`/library/${item.slug}/reader`}
icon="auto_stories"
text={format("view_scans")}
/>
)}
<div className="max-w- grid w-full gap-4">
{filterHasAttributes(item.contents.data, ["attributes"]).map((rangedContent) => (
<ContentLine
{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
? {
@ -544,8 +563,8 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
["attributes"]
).map((category) => category.attributes.short),
type:
rangedContent.attributes.content.data.attributes.type?.data?.attributes
?.titles?.[0]?.title ??
rangedContent.attributes.content.data.attributes.type?.data
?.attributes?.titles?.[0]?.title ??
prettySlug(
rangedContent.attributes.content.data.attributes.type?.data
?.attributes?.slug
@ -566,11 +585,12 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
isDefined(rangedContent.attributes.scan_set) &&
rangedContent.attributes.scan_set.length > 0
}
condensed={!isContentPanelAtLeast3xl}
displayType={isContentPanelAtLeast3xl ? "row" : "card"}
/>
))}
</div>
</div>
</div>
)}
</div>
</ContentPanel>
@ -647,7 +667,7 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
* PRIVATE COMPONENTS
*/
interface ContentLineProps {
interface ContentItemProps {
content?: {
translations: {
pre_title: string | null | undefined;
@ -662,39 +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">
<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,
@ -705,41 +718,58 @@ const ContentLine = ({
? prettySlug(content.slug, parentSlug)
: prettySlug(slug, parentSlug)}
</h3>
<p className="h-4 w-full border-b-2 border-dotted border-mid" />
<p className="text-right">{rangeStart}</p>
</div>
{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) => (
{content.categories.map((category, index) => (
<Chip key={index} text={category} />
))}
</div>
<div className="grid grid-cols-2 gap-3">
{hasScanSet || isDefined(content) ? (
<>
</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")} />
)}
</>
) : (
format("content_is_not_available")
<Button
href={`/contents/${content.slug}`}
icon="subject"
text={format("open_content")}
/>
)}
</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 grid-cols-[auto_auto_1fr_auto] items-center gap-3">
<h3>
{selectedTranslation
? prettyInlineTitle(
selectedTranslation.pre_title,
@ -750,38 +780,33 @@ const ContentLine = ({
? prettySlug(content.slug, parentSlug)
: prettySlug(slug, parentSlug)}
</h3>
</Link>
<div className="flex flex-row flex-wrap gap-1">
<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-black opacity-30" />
<p>{rangeStart}</p>
<p className="h-4 w-full border-b-2 border-dotted border-mid" />
{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) ? (
<>
<p className="text-right">{rangeStart}</p>
<div>
{hasScanSet && (
<ToolTip content={format("view_scans")}>
<Button
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
text={format("view_scans")}
icon="auto_stories"
size="small"
/>
</ToolTip>
)}
</div>
<div>
{isDefined(content) && (
<Button href={`/contents/${content.slug}`} text={format("open_content")} />
<ToolTip content={format("open_content")}>
<Button href={`/contents/${content.slug}`} icon="subject" size="small" />
</ToolTip>
)}
</div>
</>
) : (
format("content_is_not_available")
)}
</div>
</div>
);
};

View File

@ -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")}
/>
)}