Added gallery and scans pages

This commit is contained in:
DrMint 2024-05-10 11:24:44 +02:00
parent 9653747dd8
commit 470ba03402
25 changed files with 872 additions and 169 deletions

13
TODO.md
View File

@ -2,21 +2,28 @@
## Short term
- [Medias] Add Parent pages
- [Timeline] inline links to pages not working (timeline 2026/06)
- Fix inconsistency with material-icon in Payload (with or without material-icon prefix)
- [Media] display filename alongside the localized title.
- [Media] Have a title, subtitle, pretitle just like everything else
- Background images some times lack gradient at the bottom and fade in before they could load
- Number of audio players seems limited (on Chrome and Firefox)
## Mid term
- [Medias] Add Parent pages
- [Scans] Adapt size of obi based on cover/dustjacket
- [Scans] Order of cover/dustjacket/obi should be based on the book's page order.
- [RichTextContent] Add autolink block support
- Save cookies for longer than just the session
- [Scripts] Can't run the scripts using node (ts-node?)
- Support for nameless section
- [Timeline] Error if collectible not published?
- [Timeline] Handle no JS for footers
- [Timeline] Add details button in footer with credits + last updated / created
- [Collectibles] Create page for gallery
- [Collectibles] Create page for scans
- When the tags overflow, the tag group name should be align start (see http://localhost:12499/en/pages/magnitude-negative-chapter-1)
- [SDK] create a initPayload() that return a payload sdk (and stop hard wirring to ENV or node-cache)
- [Videos] see why no video on Firefox and no poster on Chrome https://v3.accords-library.com/en/videos/661b672825d380e548dbb8c8
## Long term

View File

@ -0,0 +1,118 @@
---
import Button from "components/Button.astro";
import {
type EndpointCredit,
type EndpointTagsGroup,
type PayloadImage,
type RichTextContent,
} from "src/shared/payload/payload-sdk";
import RichText from "./RichText/RichText.astro";
import TagGroups from "./TagGroups.astro";
import Credits from "./Credits.astro";
import DownloadButton from "./DownloadButton.astro";
interface Props {
previousImageHref?: string | undefined;
nextImageHref?: string | undefined;
image: PayloadImage;
title: string;
description?: RichTextContent | undefined;
tagGroups?: EndpointTagsGroup[] | undefined;
credits?: EndpointCredit[] | undefined;
filename?: string | undefined;
}
const {
nextImageHref,
previousImageHref,
image: { url, width, height },
tagGroups,
credits,
description,
title,
filename,
} = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container">
<div id="image-viewer" class:list={{ "with-buttons": previousImageHref || nextImageHref }}>
<a
class:list={{ hidden: !previousImageHref }}
href={previousImageHref}
data-astro-history="replace">
<Button icon="material-symbols:chevron-left" />
</a>
<a href={url} target="_blank"><img src={url} width={width} height={height} /></a>
<a class:list={{ hidden: !nextImageHref }} href={nextImageHref} data-astro-history="replace">
<Button icon="material-symbols:chevron-right" />
</a>
</div>
<div
id="info"
class:list={{
complex:
(tagGroups && tagGroups.length > 0) || (credits && credits.length > 0) || description,
}}>
<h1>{title}</h1>
{description && <RichText content={description} />}
<div>
{tagGroups && tagGroups.length > 0 && <TagGroups {tagGroups} />}
{credits && credits.length > 0 && <Credits credits={credits} />}
</div>
{filename && <DownloadButton href={url} filename={filename} />}
</div>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: flex;
flex-direction: column;
gap: 3em;
align-items: center;
margin-top: 3em;
& > #image-viewer {
&.with-buttons {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 1em;
place-items: center;
}
& > a.hidden {
visibility: hidden;
}
img {
max-height: 70vh;
max-width: 100%;
height: auto;
width: auto;
}
}
& > #info {
display: flex;
flex-direction: column;
gap: 1em;
align-items: center;
&.complex {
gap: 2em;
align-items: start;
}
& > h1 {
max-width: 35em;
overflow-wrap: anywhere;
}
}
}
</style>

View File

@ -1,10 +1,10 @@
---
import type { PayloadImage } from "src/shared/payload/payload-sdk";
import InlineTagGroups from "pages/[locale]/collectibles/_components/ContentsSection/InlineTagGroups.astro";
import Card from "components/Card.astro";
import { Icon } from "astro-icon/components";
import type { ComponentProps } from "astro/types";
import { getI18n } from "src/i18n/i18n";
import InlineTagGroups from "components/InlineTagGroups.astro";
interface Props {
thumbnail?: PayloadImage | undefined;

View File

@ -173,6 +173,50 @@ export const getI18n = async (locale: string) => {
);
};
const formatScanIndexShort = (index: string) => {
switch (index) {
case "cover-flap-front":
case "dustjacket-flap-front":
case "dustjacket-inside-flap-front":
case "obi-flap-front":
case "obi-inside-flap-front":
return t("collectibles.scans.shortIndex.flapFront");
case "cover-front":
case "cover-inside-front":
case "dustjacket-front":
case "dustjacket-inside-front":
case "obi-front":
case "obi-inside-front":
return t("collectibles.scans.shortIndex.front");
case "cover-spine":
case "dustjacket-spine":
case "dustjacket-inside-spine":
case "obi-spine":
case "obi-inside-spine":
return t("collectibles.scans.shortIndex.spine");
case "cover-back":
case "cover-inside-back":
case "dustjacket-back":
case "dustjacket-inside-back":
case "obi-back":
case "obi-inside-back":
return t("collectibles.scans.shortIndex.back");
case "cover-flap-back":
case "dustjacket-flap-back":
case "dustjacket-inside-flap-back":
case "obi-flap-back":
case "obi-inside-flap-back":
return t("collectibles.scans.shortIndex.flapBack");
default:
return index;
}
};
const formatEndpointSource = (source: EndpointSource) => {
switch (source.type) {
case "url":
@ -228,6 +272,20 @@ export const getI18n = async (locale: string) => {
label: getLocalizedMatch(source.folder.translations).name,
};
case "scans":
return {
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}/scans`),
typeLabel: t("global.sources.typeLabel.scans"),
label: formatInlineTitle(getLocalizedMatch(source.collectible.translations)),
};
case "gallery":
return {
href: getLocalizedUrl(`/collectibles/${source.collectible.slug}/gallery`),
typeLabel: t("global.sources.typeLabel.gallery"),
label: formatInlineTitle(getLocalizedMatch(source.collectible.translations)),
};
default:
return {
href: "/404",
@ -250,5 +308,6 @@ export const getI18n = async (locale: string) => {
formatNumber,
formatTimelineDate,
formatEndpointSource,
formatScanIndexShort,
};
};

View File

@ -89,29 +89,29 @@ export type WordingKey =
| "pages.tableOfContent.break"
| "global.languageOverride.availableLanguages"
| "timeline.title"
| "timeline.description"
| "timeline.eras.cataclysm"
| "timeline.eras.drakengard3"
| "timeline.eras.drakengard"
| "timeline.eras.drakengard2"
| "timeline.eras.drakengard3"
| "timeline.eras.nier"
| "timeline.eras.nierAutomata"
| "timeline.jumpTo"
| "timeline.notes.content"
| "timeline.eras.cataclysm"
| "timeline.description"
| "timeline.notes.title"
| "timeline.notes.content"
| "timeline.priorCataclysmNote.title"
| "timeline.priorCataclysmNote.content"
| "timeline.jumpTo"
| "timeline.year.during"
| "timeline.eventFooter.sources"
| "timeline.eventFooter.languages"
| "timeline.eventFooter.sources"
| "timeline.eventFooter.note"
| "global.sources.typeLabel.url"
| "global.sources.typeLabel.page"
| "global.sources.typeLabel.collectible"
| "global.sources.typeLabel.collectible.range.custom"
| "global.sources.typeLabel.folder"
| "global.sources.typeLabel.collectible.range.page"
| "global.sources.typeLabel.collectible.range.timestamp"
| "global.sources.typeLabel.folder"
| "global.sources.typeLabel.page"
| "global.sources.typeLabel.url"
| "global.sources.typeLabel.collectible.range.custom"
| "global.openMediaPage"
| "global.downloadButton"
| "global.previewTypes.video"
@ -119,4 +119,24 @@ export type WordingKey =
| "global.previewTypes.image"
| "global.previewTypes.audio"
| "global.previewTypes.collectible"
| "global.previewTypes.unknown";
| "global.previewTypes.unknown"
| "collectibles.scans.title"
| "collectibles.scans.subtitle"
| "collectibles.scans.shortIndex.flapFront"
| "collectibles.scans.shortIndex.front"
| "collectibles.scans.shortIndex.spine"
| "collectibles.scans.shortIndex.back"
| "collectibles.scans.shortIndex.flapBack"
| "collectibles.scans.cover"
| "collectibles.scans.coverInside"
| "collectibles.scans.dustjacket"
| "collectibles.scans.dustjacketInside"
| "collectibles.scans.obi"
| "collectibles.scans.obiInside"
| "collectibles.scans.pages"
| "collectibles.gallery.title"
| "collectibles.gallery.subtitle"
| "global.sources.typeLabel.scans"
| "collectibles.scans.dustjacket.description"
| "collectibles.scans.obi.description"
| "global.sources.typeLabel.gallery";

View File

@ -4,7 +4,7 @@ import RichText from "components/RichText/RichText.astro";
import { getI18n } from "src/i18n/i18n";
import { Collections, type EndpointCollectible } from "src/shared/payload/payload-sdk";
import { formatInlineTitle } from "src/utils/format";
import InlineTagGroups from "./InlineTagGroups.astro";
import InlineTagGroups from "../../../../../../components/InlineTagGroups.astro";
import Card from "components/Card.astro";
import AudioPlayer from "components/AudioPlayer.astro";
import VideoPlayer from "components/VideoPlayer.astro";

View File

@ -3,26 +3,27 @@ interface Props {
image: string;
title: string;
subtitle: string;
href: string;
}
const { image, title, subtitle } = Astro.props;
const { image, title, subtitle, href } = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="tile">
<a href={href}>
<img src={image} />
<div class="high-contrast-text">
<p class="title">{title}</p>
<p>{subtitle}</p>
</div>
</div>
</a>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#tile {
a {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
@ -33,6 +34,12 @@ const { image, title, subtitle } = Astro.props;
border-radius: 12px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
transition: 100ms scale;
&:hover {
scale: 102%;
}
& > div {
text-align: center;
backdrop-filter: blur(5px);

View File

@ -0,0 +1,42 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import Lightbox from "components/Lightbox.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/shared/payload/payload-sdk";
import { fetchOr404 } from "src/utils/responses";
const slug = Astro.params.slug!;
const index = Astro.params.index!;
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const galleryImage = await fetchOr404(() => payload.getCollectibleGalleryImage(slug, index));
if (galleryImage instanceof Response) {
return galleryImage;
}
const { parentPages, previousIndex, nextIndex, image } = galleryImage;
const { title, description } =
image.translations.length > 0
? getLocalizedMatch(image.translations)
: { title: image.filename, description: undefined };
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout parentPages={parentPages}>
<Lightbox
image={image}
title={title}
previousImageHref={previousIndex
? getLocalizedUrl(`/collectibles/${slug}/gallery/${previousIndex}`)
: undefined}
nextImageHref={nextIndex
? getLocalizedUrl(`/collectibles/${slug}/gallery/${nextIndex}`)
: undefined}
description={description}
filename={image.filename}
tagGroups={image.tagGroups}
credits={image.credits}
/>
</AppEmptyLayout>

View File

@ -0,0 +1,80 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import RichText from "components/RichText/RichText.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/shared/payload/payload-sdk";
import { fetchOr404 } from "src/utils/responses";
const slug = Astro.params.slug!;
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const gallery = await fetchOr404(() => payload.getCollectibleGallery(slug));
if (gallery instanceof Response) {
return gallery;
}
const { translations, parentPages, images } = gallery;
const translation = getLocalizedMatch(translations);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout parentPages={parentPages}>
<AppLayoutTitle
title={translation.title}
pretitle={translation.pretitle}
subtitle={translation.subtitle}
/>
{
translation.description && (
<div id="summary" class="high-contrast-text">
<RichText content={translation.description} />
</div>
)
}
<div>
{
images.map((image, index) => (
<a href={getLocalizedUrl(`/collectibles/${slug}/gallery/${index}`)}>
<img src={image.url} />
</a>
))
}
</div>
</AppEmptyLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
div {
margin-top: 3em;
display: flex;
gap: 2em;
flex-wrap: wrap;
a {
display: flex;
flex-direction: column;
gap: 1em;
text-align: center;
place-items: center;
transition: 100ms scale;
&:hover {
scale: 104%;
}
& > img {
max-height: 20em;
max-width: 100%;
height: auto;
width: auto;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
}
}
</style>

View File

@ -44,9 +44,6 @@ const {
} = collectible;
const translation = getLocalizedMatch(translations);
const galleryFirstImage = gallery[0];
const scansFirstImage = scans[0];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
@ -82,21 +79,23 @@ const scansFirstImage = scans[0];
<div id="gallery-scans" class="when-no-print">
{
galleryFirstImage && (
gallery && (
<ImageTile
image={galleryFirstImage.url}
title="Gallery"
subtitle={`${gallery.length} images`}
image={gallery.thumbnail.url}
title={t("collectibles.gallery.title")}
subtitle={t("collectibles.gallery.subtitle", { count: gallery.count })}
href={getLocalizedUrl(`/collectibles/${slug}/gallery`)}
/>
)
}
{
scansFirstImage && (
scans && (
<ImageTile
image={scansFirstImage.url}
title="Scans"
subtitle={`${scans.length} images`}
image={scans.thumbnail.url}
title={t("collectibles.scans.title")}
subtitle={t("collectibles.scans.subtitle", { count: scans.count })}
href={getLocalizedUrl(`/collectibles/${slug}/scans`)}
/>
)
}
@ -134,21 +133,23 @@ const scansFirstImage = scans[0];
<div id="images">
<div id="gallery-scans" class="when-no-print">
{
galleryFirstImage && (
gallery && (
<ImageTile
image={galleryFirstImage.url}
title={t("collectibles.gallery")}
subtitle={t("collectibles.imageCount", { count: gallery.length })}
image={gallery.thumbnail.url}
title={t("collectibles.gallery.title")}
subtitle={t("collectibles.gallery.subtitle", { count: gallery.count })}
href={getLocalizedUrl(`/collectibles/${slug}/gallery`)}
/>
)
}
{
scansFirstImage && (
scans && (
<ImageTile
image={scansFirstImage.url}
title={t("collectibles.scans")}
subtitle={t("collectibles.imageCount", { count: scans.length })}
image={scans.thumbnail.url}
title={t("collectibles.scans.title")}
subtitle={t("collectibles.scans.subtitle", { count: scans.count })}
href={getLocalizedUrl(`/collectibles/${slug}/scans`)}
/>
)
}

View File

@ -0,0 +1,34 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import Lightbox from "components/Lightbox.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/shared/payload/payload-sdk";
import { fetchOr404 } from "src/utils/responses";
const slug = Astro.params.slug!;
const index = Astro.params.index!;
const { formatScanIndexShort, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const scanPage = await fetchOr404(() => payload.getCollectibleScanPage(slug, index));
if (scanPage instanceof Response) {
return scanPage;
}
const { parentPages, previousIndex, nextIndex, image } = scanPage;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout parentPages={parentPages}>
<Lightbox
image={image}
title={formatScanIndexShort(index)}
previousImageHref={previousIndex
? getLocalizedUrl(`/collectibles/${slug}/scans/${previousIndex}`)
: undefined}
nextImageHref={nextIndex
? getLocalizedUrl(`/collectibles/${slug}/scans/${nextIndex}`)
: undefined}
filename={image.filename}
/>
</AppEmptyLayout>

View File

@ -0,0 +1,49 @@
---
import { getI18n } from "src/i18n/i18n";
import type { EndpointScanImage } from "src/shared/payload/payload-sdk";
interface Props {
scan: EndpointScanImage;
collectibleSlug: string;
}
const {
scan: { url, index, width, height },
collectibleSlug,
} = Astro.props;
const { getLocalizedUrl, formatScanIndexShort } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<a href={getLocalizedUrl(`/collectibles/${collectibleSlug}/scans/${index}`)}>
<img width={width} height={height} src={url} alt={index} />
<p>{formatScanIndexShort(index)}</p>
</a>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
a {
display: flex;
flex-direction: column;
gap: 1em;
text-align: center;
place-items: center;
transition: 100ms scale;
&:hover {
scale: 104%;
}
& > img {
max-height: 20em;
max-width: 100%;
height: auto;
width: auto;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
}
</style>

View File

@ -0,0 +1,215 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Credits from "components/Credits.astro";
import RichText from "components/RichText/RichText.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/shared/payload/payload-sdk";
import { fetchOr404 } from "src/utils/responses";
import ScanPreview from "./_components/ScanPreview.astro";
const slug = Astro.params.slug!;
const { getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
const scans = await fetchOr404(() => payload.getCollectibleScans(slug));
if (scans instanceof Response) {
return scans;
}
const { translations, credits, cover, pages, dustjacket, obi, parentPages } = scans;
const translation = getLocalizedMatch(translations);
const hasInsideCover = cover ? Object.keys(cover).some((value) => value.includes("inside")) : false;
const hasOutsideCover = cover
? Object.keys(cover).some((value) => !value.includes("inside"))
: false;
const hasInsideDustjacket = dustjacket
? Object.keys(dustjacket).some((value) => value.includes("inside"))
: false;
const hasOutsideDustjacket = dustjacket
? Object.keys(dustjacket).some((value) => !value.includes("inside"))
: false;
const hasInsideObi = obi ? Object.keys(obi).some((value) => value.includes("inside")) : false;
const hasOutsideObi = obi ? Object.keys(obi).some((value) => !value.includes("inside")) : false;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout parentPages={parentPages}>
<AppLayoutTitle
title={translation.title}
pretitle={translation.pretitle}
subtitle={translation.subtitle}
/>
{
translation.description && (
<div id="summary" class="high-contrast-text">
<RichText content={translation.description} />
</div>
)
}
{credits.length > 0 && <Credits credits={credits} />}
{
cover && (
<>
{hasOutsideCover && (
<section>
<h2>{t("collectibles.scans.cover")}</h2>
<div>
{cover.flapFront && <ScanPreview collectibleSlug={slug} scan={cover.flapFront} />}
{cover.front && <ScanPreview collectibleSlug={slug} scan={cover.front} />}
{cover.spine && <ScanPreview collectibleSlug={slug} scan={cover.spine} />}
{cover.back && <ScanPreview collectibleSlug={slug} scan={cover.back} />}
{cover.flapBack && <ScanPreview collectibleSlug={slug} scan={cover.flapBack} />}
</div>
</section>
)}
{hasInsideCover && (
<section>
<h2>{t("collectibles.scans.coverInside")}</h2>
<div>
{cover.insideFlapFront && (
<ScanPreview collectibleSlug={slug} scan={cover.insideFlapFront} />
)}
{cover.insideFront && <ScanPreview collectibleSlug={slug} scan={cover.insideFront} />}
{cover.insideBack && <ScanPreview collectibleSlug={slug} scan={cover.insideBack} />}
{cover.insideFlapBack && (
<ScanPreview collectibleSlug={slug} scan={cover.insideFlapBack} />
)}
</div>
</section>
)}
</>
)
}
{
dustjacket && (
<>
{hasOutsideDustjacket && (
<section>
<h2>{t("collectibles.scans.dustjacket")}</h2>
<p class="prose">{t("collectibles.scans.dustjacket.description")}</p>
<div>
{dustjacket.flapFront && (
<ScanPreview collectibleSlug={slug} scan={dustjacket.flapFront} />
)}
{dustjacket.front && <ScanPreview collectibleSlug={slug} scan={dustjacket.front} />}
{dustjacket.spine && <ScanPreview collectibleSlug={slug} scan={dustjacket.spine} />}
{dustjacket.back && <ScanPreview collectibleSlug={slug} scan={dustjacket.back} />}
{dustjacket.flapBack && (
<ScanPreview collectibleSlug={slug} scan={dustjacket.flapBack} />
)}
</div>
</section>
)}
{hasInsideDustjacket && (
<section>
<h2>{t("collectibles.scans.dustjacketInside")}</h2>
<div>
{dustjacket.insideFlapFront && (
<ScanPreview collectibleSlug={slug} scan={dustjacket.insideFlapFront} />
)}
{dustjacket.insideFront && (
<ScanPreview collectibleSlug={slug} scan={dustjacket.insideFront} />
)}
{dustjacket.insideSpine && (
<ScanPreview collectibleSlug={slug} scan={dustjacket.insideSpine} />
)}
{dustjacket.insideBack && (
<ScanPreview collectibleSlug={slug} scan={dustjacket.insideBack} />
)}
{dustjacket.insideFlapBack && (
<ScanPreview collectibleSlug={slug} scan={dustjacket.insideFlapBack} />
)}
</div>
</section>
)}
</>
)
}
{
obi && (
<>
{hasOutsideObi && (
<section>
<h2>{t("collectibles.scans.obi")}</h2>
<p class="prose">{t("collectibles.scans.obi.description")}</p>
<div>
{obi.flapFront && <ScanPreview collectibleSlug={slug} scan={obi.flapFront} />}
{obi.front && <ScanPreview collectibleSlug={slug} scan={obi.front} />}
{obi.spine && <ScanPreview collectibleSlug={slug} scan={obi.spine} />}
{obi.back && <ScanPreview collectibleSlug={slug} scan={obi.back} />}
{obi.flapBack && <ScanPreview collectibleSlug={slug} scan={obi.flapBack} />}
</div>
</section>
)}
{hasInsideObi && (
<section>
<h2>{t("collectibles.scans.obiInside")}</h2>
<div>
{obi.insideFlapFront && (
<ScanPreview collectibleSlug={slug} scan={obi.insideFlapFront} />
)}
{obi.insideFront && <ScanPreview collectibleSlug={slug} scan={obi.insideFront} />}
{obi.insideSpine && <ScanPreview collectibleSlug={slug} scan={obi.insideSpine} />}
{obi.insideBack && <ScanPreview collectibleSlug={slug} scan={obi.insideBack} />}
{obi.insideFlapBack && (
<ScanPreview collectibleSlug={slug} scan={obi.insideFlapBack} />
)}
</div>
</section>
)}
</>
)
}
{
pages.length > 0 && (
<section>
<h2>{t("collectibles.scans.pages")}</h2>
<div>
{pages.map((image) => (
<ScanPreview collectibleSlug={slug} scan={image} />
))}
</div>
</section>
)
}
</AppEmptyLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#summary {
margin-block: 2.5em;
}
section {
margin-block: 6em;
& > h2 {
font-family: var(--font-serif);
font-size: 30px;
}
& > p {
margin-top: 0.5em;
margin-bottom: 2em;
}
& > div {
margin-top: 1em;
display: flex;
gap: 2em;
flex-wrap: wrap;
}
}
</style>

View File

@ -1,9 +1,6 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import Credits from "components/Credits.astro";
import DownloadButton from "components/DownloadButton.astro";
import RichText from "components/RichText/RichText.astro";
import TagGroups from "components/TagGroups.astro";
import Lightbox from "components/Lightbox.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/shared/payload/payload-sdk";
import { fetchOr404 } from "src/utils/responses";
@ -15,7 +12,7 @@ if (image instanceof Response) {
}
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { url, width, height, filename, translations, tagGroups, credits } = image;
const { filename, translations, tagGroups, credits } = image;
const { title, description } =
translations.length > 0
@ -26,57 +23,12 @@ const { title, description } =
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout>
<div id="container">
<a href={url} target="_blank">
<img src={url} width={width} height={height} />
</a>
<div>
<h1>{title}</h1>
{description && <RichText content={description} />}
<div>
{tagGroups.length > 0 && <TagGroups {tagGroups} />}
{credits.length > 0 && <Credits credits={credits} />}
</div>
<DownloadButton href={url} filename={filename} />
</div>
</div>
<Lightbox
image={image}
title={title}
description={description}
filename={filename}
tagGroups={tagGroups}
credits={credits}
/>
</AppEmptyLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: flex;
flex-direction: column;
gap: 6em;
align-items: center;
img {
max-height: 60vh;
max-width: 100%;
height: auto;
width: auto;
}
h1 {
max-width: 35em;
overflow-wrap: anywhere;
}
& > div {
display: flex;
flex-direction: column;
gap: 2em;
align-items: start;
& > div {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 2em 6em;
width: 100%;
}
}
}
</style>

View File

@ -23,27 +23,27 @@ export interface Config {
pages: Page;
collectibles: Collectible;
folders: Folder;
'chronology-events': ChronologyEvent;
"chronology-events": ChronologyEvent;
images: Image;
audios: Audio;
'media-thumbnails': MediaThumbnail;
"media-thumbnails": MediaThumbnail;
videos: Video;
'videos-subtitles': VideoSubtitle;
'videos-channels': VideosChannel;
"videos-subtitles": VideoSubtitle;
"videos-channels": VideosChannel;
scans: Scan;
tags: Tag;
'tags-groups': TagsGroup;
'credits-roles': CreditsRole;
"tags-groups": TagsGroup;
"credits-roles": CreditsRole;
recorders: Recorder;
languages: Language;
currencies: Currency;
wordings: Wording;
'generic-contents': GenericContent;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
"generic-contents": GenericContent;
"payload-preferences": PayloadPreference;
"payload-migrations": PayloadMigration;
};
globals: {
'website-config': WebsiteConfig;
"website-config": WebsiteConfig;
};
}
/**
@ -70,8 +70,8 @@ export interface Page {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -85,8 +85,8 @@ export interface Page {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -100,7 +100,7 @@ export interface Page {
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
_status?: ("draft" | "published") | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -120,8 +120,8 @@ export interface Image {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -225,7 +225,7 @@ export interface Recorder {
username: string;
avatar?: string | Image | null;
languages?: (string | Language)[] | null;
role?: ('Admin' | 'Recorder' | 'Api')[] | null;
role?: ("Admin" | "Recorder" | "Api")[] | null;
anonymize: boolean;
email: string;
resetPasswordToken?: string | null;
@ -256,8 +256,8 @@ export interface Folder {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -281,23 +281,23 @@ export interface Folder {
files?:
| (
| {
relationTo: 'collectibles';
relationTo: "collectibles";
value: string | Collectible;
}
| {
relationTo: 'pages';
relationTo: "pages";
value: string | Page;
}
| {
relationTo: 'videos';
relationTo: "videos";
value: string | Video;
}
| {
relationTo: 'images';
relationTo: "images";
value: string | Image;
}
| {
relationTo: 'audios';
relationTo: "audios";
value: string | Audio;
}
)[]
@ -313,7 +313,7 @@ export interface Collectible {
id: string;
slug: string;
thumbnail?: string | Image | null;
nature: 'Physical' | 'Digital';
nature: "Physical" | "Digital";
languages?: (string | Language)[] | null;
tags?: (string | Tag)[] | null;
translations: {
@ -329,8 +329,8 @@ export interface Collectible {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -419,8 +419,8 @@ export interface Collectible {
pageInfoEnabled?: boolean | null;
pageInfo?: {
pageCount: number;
bindingType?: ('Paperback' | 'Hardcover') | null;
pageOrder?: ('Left to right' | 'Right to left') | null;
bindingType?: ("Paperback" | "Hardcover") | null;
pageOrder?: ("Left to right" | "Right to left") | null;
};
folders?: (string | Folder)[] | null;
parentItems?: (string | Collectible)[] | null;
@ -429,19 +429,19 @@ export interface Collectible {
| {
content:
| {
relationTo: 'pages';
relationTo: "pages";
value: string | Page;
}
| {
relationTo: 'generic-contents';
relationTo: "generic-contents";
value: string | GenericContent;
}
| {
relationTo: 'audios';
relationTo: "audios";
value: string | Audio;
}
| {
relationTo: 'videos';
relationTo: "videos";
value: string | Video;
};
range?:
@ -451,14 +451,14 @@ export interface Collectible {
end: number;
id?: string | null;
blockName?: string | null;
blockType: 'pageRange';
blockType: "pageRange";
}
| {
start: string;
end: string;
id?: string | null;
blockName?: string | null;
blockType: 'timeRange';
blockType: "timeRange";
}
| {
translations: {
@ -471,8 +471,8 @@ export interface Collectible {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -482,7 +482,7 @@ export interface Collectible {
}[];
id?: string | null;
blockName?: string | null;
blockType: 'other';
blockType: "other";
}
)[]
| null;
@ -492,7 +492,7 @@ export interface Collectible {
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
_status?: ("draft" | "published") | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -568,8 +568,8 @@ export interface Audio {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -640,8 +640,8 @@ export interface Video {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -720,8 +720,8 @@ export interface ChronologyEvent {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -735,8 +735,8 @@ export interface ChronologyEvent {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -750,7 +750,7 @@ export interface ChronologyEvent {
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
_status?: ("draft" | "published") | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -760,7 +760,7 @@ export interface UrlBlock {
url: string;
id?: string | null;
blockName?: string | null;
blockType: 'urlBlock';
blockType: "urlBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -774,13 +774,13 @@ export interface CollectibleBlock {
page: number;
id?: string | null;
blockName?: string | null;
blockType: 'page';
blockType: "page";
}
| {
timestamp: string;
id?: string | null;
blockName?: string | null;
blockType: 'timestamp';
blockType: "timestamp";
}
| {
translations: {
@ -790,13 +790,13 @@ export interface CollectibleBlock {
}[];
id?: string | null;
blockName?: string | null;
blockType: 'other';
blockType: "other";
}
)[]
| null;
id?: string | null;
blockName?: string | null;
blockType: 'collectibleBlock';
blockType: "collectibleBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -806,7 +806,7 @@ export interface PageBlock {
page: string | Page;
id?: string | null;
blockName?: string | null;
blockType: 'pageBlock';
blockType: "pageBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -830,7 +830,7 @@ export interface Wording {
export interface PayloadPreference {
id: string;
user: {
relationTo: 'recorders';
relationTo: "recorders";
value: string | Recorder;
};
key?: string | null;
@ -898,8 +898,8 @@ export interface LineBlock {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -907,7 +907,7 @@ export interface LineBlock {
};
id?: string | null;
blockName?: string | null;
blockType: 'lineBlock';
blockType: "lineBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -922,8 +922,8 @@ export interface CueBlock {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -931,7 +931,7 @@ export interface CueBlock {
};
id?: string | null;
blockName?: string | null;
blockType: 'cueBlock';
blockType: "cueBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -941,17 +941,17 @@ export interface TranscriptBlock {
lines: (LineBlock | CueBlock)[];
id?: string | null;
blockName?: string | null;
blockType: 'transcriptBlock';
blockType: "transcriptBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "BreakBlock".
*/
export interface BreakBlock {
type: 'Scene break' | 'Empty space' | 'Solid line' | 'Dotted line';
type: "Scene break" | "Empty space" | "Solid line" | "Dotted line";
id?: string | null;
blockName?: string | null;
blockType: 'breakBlock';
blockType: "breakBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -966,8 +966,8 @@ export interface SectionBlock {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
@ -975,11 +975,10 @@ export interface SectionBlock {
};
id?: string | null;
blockName?: string | null;
blockType: 'sectionBlock';
blockType: "sectionBlock";
}
/////////////// CONSTANTS ///////////////
@ -1495,8 +1494,8 @@ export type EndpointCollectible = {
languages: string[];
backgroundImage?: EndpointImage;
nature: CollectibleNature;
gallery: EndpointImage[];
scans: PayloadImage[];
gallery?: { count: number; thumbnail: EndpointImage };
scans?: { count: number; thumbnail: PayloadImage };
urls: { url: string; label: string }[];
price?: {
amount: number;
@ -1560,6 +1559,104 @@ export type EndpointCollectible = {
parentPages: EndpointSource[];
};
export type EndpointCollectibleScans = {
slug: string;
thumbnail?: EndpointImage;
translations: {
language: string;
pretitle?: string;
title: string;
subtitle?: string;
description?: RichTextContent;
}[];
credits: EndpointCredit[];
cover?: {
front?: EndpointScanImage;
spine?: EndpointScanImage;
back?: EndpointScanImage;
insideFront?: EndpointScanImage;
insideBack?: EndpointScanImage;
flapFront?: EndpointScanImage;
flapBack?: EndpointScanImage;
insideFlapFront?: EndpointScanImage;
insideFlapBack?: EndpointScanImage;
};
dustjacket?: {
front?: EndpointScanImage;
spine?: EndpointScanImage;
back?: EndpointScanImage;
insideFront?: EndpointScanImage;
insideSpine?: EndpointScanImage;
insideBack?: EndpointScanImage;
flapFront?: EndpointScanImage;
flapBack?: EndpointScanImage;
insideFlapFront?: EndpointScanImage;
insideFlapBack?: EndpointScanImage;
};
obi?: {
front?: EndpointScanImage;
spine?: EndpointScanImage;
back?: EndpointScanImage;
insideFront?: EndpointScanImage;
insideSpine?: EndpointScanImage;
insideBack?: EndpointScanImage;
flapFront?: EndpointScanImage;
flapBack?: EndpointScanImage;
insideFlapFront?: EndpointScanImage;
insideFlapBack?: EndpointScanImage;
};
pages: EndpointScanImage[];
parentPages: EndpointSource[];
};
export type EndpointCollectibleGallery = {
slug: string;
thumbnail?: EndpointImage;
translations: {
language: string;
pretitle?: string;
title: string;
subtitle?: string;
description?: RichTextContent;
}[];
images: EndpointImage[];
parentPages: EndpointSource[];
};
export type EndpointCollectibleGalleryImage = {
slug: string;
thumbnail?: EndpointImage;
translations: {
language: string;
pretitle?: string;
title: string;
subtitle?: string;
}[];
image: EndpointImage;
previousIndex?: string;
nextIndex?: string;
parentPages: EndpointSource[];
};
export type EndpointCollectibleScanPage = {
slug: string;
thumbnail?: EndpointImage;
translations: {
language: string;
pretitle?: string;
title: string;
subtitle?: string;
}[];
image: EndpointScanImage;
previousIndex?: string;
nextIndex?: string;
parentPages: EndpointSource[];
};
export type EndpointScanImage = PayloadImage & {
index: string;
};
export type TableOfContentEntry = {
prefix: string;
title: string;
@ -1599,7 +1696,9 @@ export type EndpointSource =
| { type: "custom"; translations: { language: string; note: string }[] };
}
| { type: "page"; page: EndpointPage }
| { type: "folder"; folder: EndpointFolder };
| { type: "folder"; folder: EndpointFolder }
| { type: "scans"; collectible: EndpointCollectible }
| { type: "gallery"; collectible: EndpointCollectible };
export type EndpointMedia = {
id: string;
@ -1678,12 +1777,32 @@ export const payload = {
await (await request(payloadApiUrl(Collections.Pages, `slug/${slug}`))).json(),
getCollectible: async (slug: string): Promise<EndpointCollectible> =>
await (await request(payloadApiUrl(Collections.Collectibles, `slug/${slug}`))).json(),
getCollectibleScans: async (slug: string): Promise<EndpointCollectibleScans> =>
await (await request(payloadApiUrl(Collections.Collectibles, `slug/${slug}/scans`))).json(),
getCollectibleScanPage: async (
slug: string,
index: string
): Promise<EndpointCollectibleScanPage> =>
await (
await request(payloadApiUrl(Collections.Collectibles, `slug/${slug}/scans/${index}`))
).json(),
getCollectibleGallery: async (slug: string): Promise<EndpointCollectibleGallery> =>
await (await request(payloadApiUrl(Collections.Collectibles, `slug/${slug}/gallery`))).json(),
getCollectibleGalleryImage: async (
slug: string,
index: string
): Promise<EndpointCollectibleGalleryImage> =>
await (
await request(payloadApiUrl(Collections.Collectibles, `slug/${slug}/gallery/${index}`))
).json(),
getChronologyEvents: async (): Promise<EndpointChronologyEvent[]> =>
await (await request(payloadApiUrl(Collections.ChronologyEvents, `all`))).json(),
getChronologyEventByID: async (id: string): Promise<EndpointChronologyEvent> =>
await (await request(payloadApiUrl(Collections.ChronologyEvents, `id/${id}`))).json(),
getImageByID: async (id: string): Promise<EndpointImage> =>
await (await request(payloadApiUrl(Collections.Images, `id/${id}`))).json(),
getScanByID: async (id: string): Promise<PayloadImage> =>
await (await request(payloadApiUrl(Collections.Scans, `id/${id}`))).json(),
getAudioByID: async (id: string): Promise<EndpointAudio> =>
await (await request(payloadApiUrl(Collections.Audios, `id/${id}`))).json(),
getVideoByID: async (id: string): Promise<EndpointVideo> =>