diff --git a/.env.example b/.env.example new file mode 100755 index 0000000..b1315e0 --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 200b12b..5ba7ea5 100644 --- a/README.md +++ b/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: diff --git a/public/local-data/websiteInterfaces.json b/public/local-data/websiteInterfaces.json index a45e05f..3b6890a 100644 --- a/public/local-data/websiteInterfaces.json +++ b/public/local-data/websiteInterfaces.json @@ -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 } } ] diff --git a/src/components/PreviewCard.tsx b/src/components/PreviewCard.tsx index 11c4242..4fa8b55 100644 --- a/src/components/PreviewCard.tsx +++ b/src/components/PreviewCard.tsx @@ -204,7 +204,7 @@ export const PreviewCard = ({ )} {subtitle && } - {description && } + {description && } {bottomChips && bottomChips.length > 0 && (
{ return (
+ 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">

{format("empty_folder_message")}

diff --git a/src/pages/library/[slug]/index.tsx b/src/pages/library/[slug]/index.tsx index 0e65787..b529f78 100644 --- a/src/pages/library/[slug]/index.tsx +++ b/src/pages/library/[slug]/index.tsx @@ -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" />
)}
-

{item.title}

+

{item.title}

{isDefinedAndNotEmpty(item.subtitle) &&

{item.subtitle}

}
@@ -517,58 +520,75 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => { )} - {item.contents && item.contents.data.length > 0 && ( + {hasContentSection && (

{format("contents")}

- {displayOpenScans && ( -
-
- )} -
- {filterHasAttributes(item.contents.data, ["attributes"]).map((rangedContent) => ( - ({ - 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} +
+ {hasContentScans && ( +
+
+
+ {filterHasAttributes(item.contents?.data, ["attributes"]).map((rangedContent) => ( + ({ + 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"} + /> + ))} +
)} @@ -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["translations"][number]) => item.language, + (item: NonNullable["translations"][number]) => item.language, [] ), }); - if (condensed) { + if (displayType === "card") { return ( -
-
- {content?.type && } -

-

{rangeStart}

-
- -

- {selectedTranslation - ? prettyInlineTitle( - selectedTranslation.pre_title, - selectedTranslation.title, - selectedTranslation.subtitle - ) - : content - ? prettySlug(content.slug, parentSlug) - : prettySlug(slug, parentSlug)} -

-
- {content?.categories?.map((category, index) => ( - - ))} -
-
- {hasScanSet || isDefined(content) ? ( - <> - {hasScanSet && ( -
-
- ); - } - - return ( -
-
- -

+
+
+

{selectedTranslation ? prettyInlineTitle( selectedTranslation.pre_title, @@ -750,38 +718,95 @@ const ContentLine = ({ ? prettySlug(content.slug, parentSlug) : prettySlug(slug, parentSlug)}

- -
- {content?.categories?.map((category, index) => ( - - ))} +

+

{rangeStart}

-

-

{rangeStart}

- {content?.type && } -
-
- - {hasScanSet || isDefined(content) ? ( - <> + {content && ( +
+ {content.categories && ( +
+

{format("category", { count: content.categories.length })}

+
+ {content.categories.map((category, index) => ( + + ))} +
+
+ )} + + {content.type && ( +
+

{format("type", { count: 1 })}

+ +
+ )} +
+ )} + + {(hasScanSet || content) && ( +
{hasScanSet && (
)}
-
+ ); + } + + return ( + <> +
+

+ {selectedTranslation + ? prettyInlineTitle( + selectedTranslation.pre_title, + selectedTranslation.title, + selectedTranslation.subtitle + ) + : content + ? prettySlug(content.slug, parentSlug) + : prettySlug(slug, parentSlug)} +

+
+ {content?.categories?.map((category, index) => ( + + ))} +
+

+ {content?.type && } +

+

{rangeStart}

+
+ {hasScanSet && ( + +
+
+ {isDefined(content) && ( + +
+ ); }; diff --git a/src/pages/library/[slug]/reader.tsx b/src/pages/library/[slug]/reader.tsx index 0d54934..b7afb52 100644 --- a/src/pages/library/[slug]/reader.tsx +++ b/src/pages/library/[slug]/reader.tsx @@ -366,6 +366,14 @@ const LibrarySlug = ({ sendAnalytics("Reader", "Reset all options"); }} /> + + {item.download_available && ( +