Handle collectible pages

This commit is contained in:
DrMint 2024-03-08 22:41:33 +01:00
parent 6c1956ce5c
commit 3c7ce915f1
37 changed files with 1656 additions and 470 deletions

9
TODO.md Normal file
View File

@ -0,0 +1,9 @@
# Accord's Library v3.0
## Short term
- Translate new wording keys
## Long term
- Anonymous comments

BIN
bun.lockb

Binary file not shown.

View File

@ -21,27 +21,27 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.5.6", "@astrojs/check": "^0.5.6",
"@astrojs/node": "^8.2.1", "@astrojs/node": "^8.2.3",
"@fontsource-variable/murecho": "^5.0.17", "@fontsource-variable/murecho": "^5.0.18",
"@fontsource-variable/vollkorn": "^5.0.19", "@fontsource-variable/vollkorn": "^5.0.20",
"accept-language": "^3.0.18", "accept-language": "^3.0.18",
"astro": "4.4.6", "astro": "4.4.15",
"astro-icon": "^1.1.0", "astro-icon": "^1.1.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"ua-parser-js": "^1.0.37" "ua-parser-js": "^1.0.37"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/material-symbols": "^1.1.73", "@iconify-json/material-symbols": "^1.1.74",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"astro-meta-tags": "^0.2.1", "astro-meta-tags": "^0.2.1",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.18",
"bun-types": "^1.0.29", "bun-types": "^1.0.30",
"npm-check-updates": "^16.14.15", "npm-check-updates": "^16.14.15",
"postcss-preset-env": "^9.4.0", "postcss-preset-env": "^9.5.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0", "prettier-plugin-astro": "^0.13.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.3.3" "typescript": "^5.4.2"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

View File

@ -10,37 +10,48 @@ interface Props {
const { src, alt } = Astro.props; const { src, alt } = Astro.props;
const uniqueId = getRandomId(); const uniqueId = getRandomId();
const styleNoScript = `
<style>
#${uniqueId} {
opacity: 1;
transition: unset;
}
</style>`;
--- ---
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<img id={uniqueId} src={src} alt={alt} class="when-no-print" /> <img id={uniqueId} src={src} alt={alt} class="when-no-print when-js" />
<noscript set:html={styleNoScript} /> <img src={src} alt={alt} class="when-no-print when-no-js" />
{/* ------------------------------------------- CSS -------------------------------------------- */} {/* ------------------------------------------- CSS -------------------------------------------- */}
<style> <style>
img { img {
opacity: 0;
transition: 3s opacity;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
z-index: -1; bottom: 0;
width: 100%;
height: 100vh; height: 100vh;
object-fit: cover; object-fit: cover;
object-position: 50% 0; object-position: 50% 0;
width: 100%;
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 100%); mask-image: linear-gradient(to bottom, rgba(0 0 0 / 30%) 0%, transparent 100%);
@media (min-width: 110vh) {
mask-image: linear-gradient(
to bottom,
rgba(0 0 0 / 30%) 0%,
rgba(0 0 0 / 5%) 100vh,
transparent 100%
);
height: 100%;
max-height: 100vw;
}
user-select: none;
&.when-js {
opacity: 0;
transition: 3s opacity;
}
} }
</style> </style>

View File

@ -38,19 +38,12 @@ const { currentTheme } = Astro.locals;
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27231e" /> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27231e" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<style is:global>
.when-no-js {
display: none;
}
</style>
<noscript> <noscript>
<style is:global> <style is:global>
.when-js { .when-js {
display: none !important; display: none !important;
} visibility: none !important;
.when-no-js { opacity: 0 !important;
display: initial !important;
} }
</style> </style>
</noscript> </noscript>
@ -224,10 +217,17 @@ const { currentTheme } = Astro.locals;
.high-contrast-text { .high-contrast-text {
text-shadow: 0 0 0.6em var(--color-elevation-0); text-shadow: 0 0 0.6em var(--color-elevation-0);
} }
body {
margin: clamp(12px, 3vmin, 24px) clamp(24px, 4vw, 64px);
}
} }
html { html {
position: relative;
color: var(--color-base-1000); color: var(--color-base-1000);
min-height: 100vb;
display: flex;
@media screen { @media screen {
background-color: var(--color-base-150); background-color: var(--color-base-150);
@ -235,8 +235,7 @@ const { currentTheme } = Astro.locals;
} }
body { body {
margin: clamp(12px, 3vmin, 24px) clamp(24px, 4vw, 64px); flex: 1;
min-height: 100vb;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -327,6 +326,24 @@ const { currentTheme } = Astro.locals;
} }
} }
.pressable-link {
text-decoration: underline dotted 0.1em;
text-decoration-color: transparent;
transition-duration: 150ms;
transition-property: text-decoration-color, color;
&:hover {
color: var(--color-base-750);
text-decoration-color: var(--color-base-650);
}
&:active {
color: var(--color-base-650);
text-decoration-color: var(--color-base-550);
}
}
.pressable-label { .pressable-label {
text-decoration: none; text-decoration: none;
flex-shrink: 0; flex-shrink: 0;
@ -448,3 +465,13 @@ const { currentTheme } = Astro.locals;
} }
} }
</style> </style>
{/* ------------------------------------------- JS --------------------------------------------- */}
<script is:inline>
Array.from(document.querySelectorAll(".when-no-js")).forEach((node) => node.remove());
document.addEventListener("astro:before-swap", ({ newDocument }) => {
Array.from(newDocument.querySelectorAll(".when-no-js")).forEach((node) => node.remove());
});
</script>

View File

@ -44,7 +44,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<a href="/settings"> <a href="/settings">
<Button <Button
icon="material-symbols:settings-outline" icon="material-symbols:settings-outline"
ariaLabel={t("header.topbar.search.tooltip")} ariaLabel={t("header.topbar.settings.tooltip")}
/> />
</a> </a>
</div> </div>

View File

@ -17,13 +17,20 @@ switch (parentPage.collection) {
href = getLocalizedUrl(`/folders/${parentPage.slug}`); href = getLocalizedUrl(`/folders/${parentPage.slug}`);
break; break;
case Collections.Collectibles:
href = getLocalizedUrl(`/collectibles/${parentPage.slug}`);
break;
default: default:
href = "/404";
break; break;
} }
--- ---
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
{/* TODO: Not use the tag but actual translation */}
<a href={href}><span>{parentPage.tag}</span>{translation.name}</a> <a href={href}><span>{parentPage.tag}</span>{translation.name}</a>
{/* ------------------------------------------- CSS -------------------------------------------- */} {/* ------------------------------------------- CSS -------------------------------------------- */}

View File

@ -18,6 +18,7 @@ const { t } = await getI18n(Astro.locals.currentLocale);
<Tooltip trigger="click"> <Tooltip trigger="click">
<div id="tooltip-content" slot="tooltip-content"> <div id="tooltip-content" slot="tooltip-content">
{/* TODO: Translate */}
<p>This content is part of these pages:</p> <p>This content is part of these pages:</p>
{parentPages.map((parentPage) => <ParentPageLink parentPage={parentPage} />)} {parentPages.map((parentPage) => <ParentPageLink parentPage={parentPage} />)}
</div> </div>

View File

@ -1,5 +1,5 @@
--- ---
import type { SpacerBlock } from "src/shared/payload/payload-sdk"; import { SpacerSizes, type SpacerBlock } from "src/shared/payload/payload-sdk";
interface Props { interface Props {
block: SpacerBlock; block: SpacerBlock;
@ -7,11 +7,11 @@ interface Props {
const { block } = Astro.props; const { block } = Astro.props;
const spaceSizeToRem: Record<SpacerBlock["size"], number> = { const spaceSizeToRem: Record<SpacerSizes, number> = {
Small: 1, [SpacerSizes.Small]: 1,
Medium: 2, [SpacerSizes.Medium]: 2,
Large: 4, [SpacerSizes.Large]: 4,
XLarge: 8, [SpacerSizes.XLarge]: 8,
}; };
--- ---

View File

@ -20,7 +20,7 @@ if (values.length === 0) return;
<p>{title}</p> <p>{title}</p>
</div> </div>
<div id="values"> <div id="values">
{values.map((value) => <div class="pill">{value}</div>)} {values.map((value) => <div>{value}</div>)}
</div> </div>
</div> </div>
@ -54,12 +54,13 @@ if (values.length === 0) return;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
& > .pill { & > div {
border: 1px solid var(--color-base-1000); border: 1px solid var(--color-base-1000);
border-radius: 9999px; border-radius: 9999px;
padding-top: 0.15em; padding-top: 0.15em;
padding-bottom: 0.25em; padding-bottom: 0.25em;
padding-inline: 0.6em; padding-inline: 0.6em;
backdrop-filter: blur(10px);
} }
} }
} }

View File

@ -0,0 +1,73 @@
---
import { getI18n } from "src/i18n/i18n";
import type { EndpointCollectiblePreview } from "src/shared/payload/payload-sdk";
interface Props {
collectible: EndpointCollectiblePreview;
}
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const {
collectible: { slug, translations, thumbnail },
} = Astro.props;
const { title, pretitle, subtitle } = getLocalizedMatch(translations);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<a href={getLocalizedUrl(`/collectibles/${slug}`)} class="pressable">
{
thumbnail && (
<img src={thumbnail.url} width={thumbnail.width} height={thumbnail.height} alt="" />
)
}
<p>
{pretitle && <span id="pretitle">{pretitle}&nbsp;</span>}
<span id="title">{title}&nbsp;</span>
{subtitle && <span id="subtitle">{subtitle}</span>}
</p>
</a>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
a {
padding: 1em;
border-radius: 1em;
color: var(--color-base-1000);
}
img {
width: 100%;
height: auto;
margin-bottom: 1em;
}
p {
line-height: 0.8;
display: grid;
overflow-wrap: anywhere;
font-size: clamp(0.5em, 0.35em + 0.75vw, 1em);
font-weight: 800;
& > #pretitle {
font-family: var(--font-sans-serifs);
font-weight: 400;
margin-bottom: 0.8em;
}
& > #title {
font-family: var(--font-serif);
font-size: 200%;
}
& > #subtitle {
font-family: var(--font-serif);
font-weight: 600;
margin-top: 0.5em;
}
}
</style>

View File

@ -0,0 +1,73 @@
---
import { getI18n } from "src/i18n/i18n";
import type { EndpointPagePreview } from "src/shared/payload/payload-sdk";
interface Props {
page: EndpointPagePreview;
}
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const {
page: { slug, translations, thumbnail },
} = Astro.props;
const { title, pretitle, subtitle } = getLocalizedMatch(translations);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<a href={getLocalizedUrl(`/pages/${slug}`)} class="pressable">
{
thumbnail && (
<img src={thumbnail.url} width={thumbnail.width} height={thumbnail.height} alt="" />
)
}
<p>
{pretitle && <span id="pretitle">{pretitle}&nbsp;</span>}
<span id="title">{title}&nbsp;</span>
{subtitle && <span id="subtitle">{subtitle}</span>}
</p>
</a>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
a {
padding: 1em;
border-radius: 1em;
color: var(--color-base-1000);
}
img {
width: 100%;
height: auto;
margin-bottom: 1em;
}
p {
line-height: 0.8;
display: grid;
overflow-wrap: anywhere;
font-size: clamp(0.5em, 0.35em + 0.75vw, 1em);
font-weight: 800;
& > #pretitle {
font-family: var(--font-sans-serifs);
font-weight: 400;
margin-bottom: 0.8em;
}
& > #title {
font-family: var(--font-serif);
font-size: 200%;
}
& > #subtitle {
font-family: var(--font-serif);
font-weight: 600;
margin-top: 0.5em;
}
}
</style>

View File

@ -11,7 +11,7 @@ const { entry } = Astro.props;
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<li data-prefix={entry.prefix}> <li data-prefix={entry.prefix}>
<a href={`#${entry.prefix}`}>{entry.title}</a> <a href={`#${entry.prefix}`} class="pressable-link">{entry.title}</a>
{ {
entry.children.length > 0 && ( entry.children.length > 0 && (
<ol> <ol>
@ -28,21 +28,6 @@ const { entry } = Astro.props;
<style> <style>
a { a {
font-weight: 500; font-weight: 500;
text-decoration: underline dotted 0.1em;
text-decoration-color: transparent;
transition-duration: 150ms;
transition-property: text-decoration-color, color;
&:hover {
color: var(--color-base-750);
text-decoration-color: var(--color-base-650);
}
&:active {
color: var(--color-base-650);
text-decoration-color: var(--color-base-550);
}
} }
li { li {

View File

@ -10,19 +10,22 @@ const { tagGroups } = Astro.props;
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<div>{tagGroups.map((tag) => <TagGroup {...tag} />)}</div> <div>
{tagGroups.map((tag) => <TagGroup {...tag} />)}
<slot />
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */} {/* ------------------------------------------- CSS -------------------------------------------- */}
<style> <style>
div { div {
@media (max-width: 35rem) {
margin-block: 5em;
gap: 2em;
}
margin-block: 2em;
display: grid; display: grid;
gap: 1em; gap: 2em;
margin-block: 2em;
@media (max-width: 35rem) {
gap: 3.5em;
margin-block: 3.5em;
}
} }
</style> </style>

View File

@ -138,5 +138,48 @@ export const getI18n = async (locale: string) => {
return getLocalizedMatch(tag.translations).name; return getLocalizedMatch(tag.translations).name;
}; };
return { t, getLocalizedMatch, getLocalizedUrl, formatTag, formatTagsGroup }; const formatPrice = (price: { amount: number; currency: string }): string =>
price.amount.toLocaleString(locale, { style: "currency", currency: price.currency });
const formatDate = (date: Date): string =>
date.toLocaleDateString(locale, { dateStyle: "medium" });
const formatInches = (sizeInMm: number): string => {
return (
(sizeInMm * 0.039370078740157).toLocaleString(locale, { maximumFractionDigits: 2 }) + " in"
);
};
const formatMillimeters = (sizeInMm: number): string => {
return sizeInMm.toLocaleString(locale, { maximumFractionDigits: 0 }) + " mm";
};
const formatPounds = (weightInGrams: number): string => {
return (
(weightInGrams * 0.002204623).toLocaleString(locale, { maximumFractionDigits: 2 }) + " lb"
);
};
const formatGrams = (weightInGrams: number): string => {
return weightInGrams.toLocaleString(locale, { maximumFractionDigits: 0 }) + " g";
};
const formatNumber = (number: number, options?: Intl.NumberFormatOptions): string => {
return number.toLocaleString(locale, options);
};
return {
t,
getLocalizedMatch,
getLocalizedUrl,
formatTag,
formatTagsGroup,
formatPrice,
formatDate,
formatInches,
formatPounds,
formatGrams,
formatMillimeters,
formatNumber,
};
}; };

View File

@ -50,4 +50,28 @@ export type WordingKey =
| "footer.license.description" | "footer.license.description"
| "footer.license.icons.tooltip" | "footer.license.icons.tooltip"
| "footer.disclaimer" | "footer.disclaimer"
| "header.nav.parentPages.label"; | "header.nav.parentPages.label"
| "collectibles.releaseDate"
| "collectibles.size"
| "collectibles.size.width"
| "collectibles.size.height"
| "collectibles.size.thickness"
| "collectibles.availability.available"
| "collectibles.availability.notAvailable.future"
| "collectibles.availability.notAvailable.past"
| "collectibles.availability.notAvailable.noPrice"
| "collectibles.availability.notAvailable"
| "collectibles.price"
| "collectibles.price.free"
| "collectibles.bookFormat"
| "collectibles.bookFormat.pageCount"
| "collectibles.bookFormat.binding.paperback"
| "collectibles.bookFormat.binding.hardcover"
| "collectibles.bookFormat.binding.readingDirection.leftToRight"
| "collectibles.bookFormat.binding.readingDirection.rightToLeft"
| "collectibles.gallery"
| "collectibles.scans"
| "collectibles.imageCount"
| "header.topbar.settings.tooltip"
| "collectibles.contents"
| "collectibles.weight";

View File

@ -76,9 +76,13 @@ const translation = getLocalizedMatch(page.translations);
<Credits translators={translation.translators} proofreaders={translation.proofreaders} /> <Credits translators={translation.translators} proofreaders={translation.proofreaders} />
</div> </div>
<div class="when-not-large meta-container"> {
<TableOfContent toc={translation.toc} /> translation.toc.length > 0 && (
</div> <div class="when-not-large meta-container">
<TableOfContent toc={translation.toc} />
</div>
)
}
<hr /> <hr />
<div id="text"> <div id="text">
@ -110,10 +114,14 @@ const translation = getLocalizedMatch(page.translations);
/> />
) )
} }
<Credits translators={translation.translators} proofreaders={translation.proofreaders} /> <Credits
translators={translation.translators}
transcribers={translation.transcribers}
proofreaders={translation.proofreaders}
/>
</div> </div>
<TableOfContent toc={translation.toc} /> {translation.toc.length > 0 && <TableOfContent toc={translation.toc} />}
</div> </div>
</div> </div>
</MasoTarget> </MasoTarget>

View File

@ -0,0 +1,269 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import RichText from "components/RichText/RichText.astro";
import TagGroups from "components/TagGroups.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/shared/payload/payload-sdk";
import { fetchOr404 } from "src/utils/responses";
import ImageTile from "./_components/ImageTile.astro";
import PriceInfo from "./_components/PriceInfo.astro";
import SizeInfo from "./_components/SizeInfo.astro";
import ReleaseDateInfo from "./_components/ReleaseDateInfo.astro";
import PageInfo from "./_components/PageInfo.astro";
import AvailabilityInfo from "./_components/AvailabilityInfo.astro";
import WeightInfo from "./_components/WeightInfo.astro";
import SubitemSection from "./_components/SubitemSection.astro";
import ContentsSection from "./_components/ContentsSection/ContentsSection.astro";
const { slug } = Astro.params;
const { getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
const collectible = await fetchOr404(() => payload.getCollectible(slug!));
if (collectible instanceof Response) {
return collectible;
}
const {
translations,
thumbnail,
size,
price,
releaseDate,
pageInfo,
urls,
weight,
backgroundImage,
gallery,
scans,
subitems,
parentPages,
tagGroups,
contents,
} = collectible;
const translation = getLocalizedMatch(translations);
const galleryFirstImage = gallery[0];
const scansFirstImage = scans[0];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout
parentPages={parentPages}
backgroundIllustration={backgroundImage?.url ?? thumbnail?.url}>
<div id="layout">
<div id="left">
<AppLayoutTitle
title={translation.title}
pretitle={translation.pretitle}
subtitle={translation.subtitle}
/>
<div id="images" class="when-not-large">
{
thumbnail && (
<img
id="thumbnail"
src={thumbnail.url}
width={thumbnail.width}
height={thumbnail.height}
/>
)
}
<div id="gallery-scans" class="when-no-print">
{
galleryFirstImage && (
<ImageTile
image={galleryFirstImage.url}
title="Gallery"
subtitle={`${gallery.length} images`}
/>
)
}
{
scansFirstImage && (
<ImageTile
image={scansFirstImage.url}
title="Scans"
subtitle={`${scans.length} images`}
/>
)
}
</div>
</div>
{
translation.description && (
<div id="summary" class="high-contrast-text">
<RichText content={translation.description} />
</div>
)
}
<TagGroups tagGroups={tagGroups}>
{releaseDate && <ReleaseDateInfo releaseDate={releaseDate} />}
{price && <PriceInfo price={price} />}
<AvailabilityInfo urls={urls} price={price !== undefined} releaseDate={releaseDate} />
{size && <SizeInfo size={size} />}
{weight && <WeightInfo weight={weight} />}
{pageInfo && <PageInfo pageInfo={pageInfo} />}
</TagGroups>
{subitems.length > 0 && <SubitemSection subitems={subitems} />}
{contents.length > 0 && <ContentsSection contents={contents} />}
</div>
<div id="right" class="when-large">
<div id="images">
<div id="gallery-scans" class="when-no-print">
{
galleryFirstImage && (
<ImageTile
image={galleryFirstImage.url}
title={t("collectibles.gallery")}
subtitle={t("collectibles.imageCount", { count: gallery.length })}
/>
)
}
{
scansFirstImage && (
<ImageTile
image={scansFirstImage.url}
title={t("collectibles.scans")}
subtitle={t("collectibles.imageCount", { count: scans.length })}
/>
)
}
</div>
{
thumbnail && (
<img
id="thumbnail"
src={thumbnail.url}
width={thumbnail.width}
height={thumbnail.height}
/>
)
}
</div>
</div>
</div>
</AppEmptyLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#layout {
display: grid;
justify-content: space-between;
container-type: inline-size;
@media (min-width: 80rem) {
grid-template-columns: 35rem 35rem;
}
& > #left {
& > #images {
display: grid;
place-content: start;
place-items: start;
margin-block: 2em;
gap: clamp(1em, 0.5em + 3vw, 2em);
grid-template-columns: 1fr;
@media (max-width: 23rem) {
gap: 2.5em;
}
@media (min-width: 52rem) {
grid-template-columns: 35rem 10rem;
}
& > #thumbnail {
width: 100%;
height: auto;
box-shadow: 0 5px 20px -10px var(--color-shadow);
max-width: 35rem;
}
& > #gallery-scans {
display: flex;
max-width: 35rem;
flex-direction: column;
gap: 2.5em;
width: 100%;
> :global(div) {
aspect-ratio: 2 / 1;
}
@media (min-width: 23rem) {
gap: clamp(1em, 0.5em + 3vw, 2em);
flex-direction: row;
> :global(div) {
aspect-ratio: 1 / 1;
}
@media (min-width: 52rem) {
max-width: 15rem;
flex-direction: column;
}
}
}
}
& > #summary {
backdrop-filter: blur(5px);
padding: 1.5em;
margin: -1.5em;
margin-block: 1em;
border-radius: 3em;
}
}
& > #right {
& > #images {
display: grid;
grid-template-columns: 10rem 1fr;
gap: 1em;
& > #gallery-scans {
display: flex;
flex-direction: column;
gap: 1em;
}
& > #thumbnail {
width: 100%;
height: auto;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
}
}
}
.when-large {
@media (max-width: 80rem) {
display: none !important;
}
}
.when-not-large {
@media (min-width: 80rem) {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,106 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
import type { EndpointCollectible } from "src/shared/payload/payload-sdk";
interface Props {
urls: EndpointCollectible["urls"];
releaseDate?: string | undefined;
price?: boolean;
}
const { price = false, urls, releaseDate } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
const title = (() => {
if (urls.length > 0) return t("collectibles.availability.available");
if (price) {
if (!releaseDate) return t("collectibles.availability.notAvailable");
const release = new Date(releaseDate);
if (release > new Date()) {
return t("collectibles.availability.notAvailable.future");
} else {
return t("collectibles.availability.notAvailable.past");
}
}
return t("collectibles.availability.notAvailable.noPrice");
})();
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container">
<div id="title">
<Icon name="material-symbols:shopping-cart-outline" width={24} height={24} />
<p>{title}</p>
</div>
{
urls.length > 0 && (
<div id="values">
{urls.map(({ label, url }) => (
<a target="_blank" rel="noopener noreferrer" href={url}>
{label}
</a>
))}
</div>
)
}
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5em 1em;
align-items: center;
@media (max-width: 35em) {
grid-template-columns: 1fr;
}
& > #title {
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
}
}
& > #values {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 0.35em;
& > a {
border: 1px solid var(--color-base-1000);
border-radius: 9999px;
padding-top: 0.15em;
padding-bottom: 0.25em;
padding-inline: 0.6em;
backdrop-filter: blur(10px);
transition-duration: 150ms;
transition-property: border-color, color;
&:hover {
color: var(--color-base-750);
border-color: var(--color-base-750);
}
&:active {
color: var(--color-base-650);
border-color: var(--color-base-650);
}
}
}
}
</style>

View File

@ -0,0 +1,93 @@
---
import ErrorMessage from "components/ErrorMessage.astro";
import RichText from "components/RichText/RichText.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointCollectible } from "src/shared/payload/payload-sdk";
import { formatInlineTitle } from "src/utils/format";
interface Props {
content: EndpointCollectible["contents"][number];
}
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const {
content: { content, range },
} = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="row">
<div id="title">
{
content.relationTo === "generic-contents" ? (
<p>{getLocalizedMatch(content.value.translations).name}</p>
) : content.relationTo === "pages" ? (
<a href={getLocalizedUrl(`/pages/${content.value.slug}`)} class="pressable-link">
{formatInlineTitle(getLocalizedMatch(content.value.translations))}
</a>
) : (
<ErrorMessage
title="Unknown content type"
description="Please contact website technical administrator."
/>
)
}
</div>
<div id="dots"></div>
<div id="range">
{
range && (
<>
{range.type === "pageRange" ? (
range.start
) : range.type === "timeRange" ? (
range.start
) : range.type === "other" ? (
<RichText content={getLocalizedMatch(range.translations).note} />
) : (
<ErrorMessage
title="Unknown range type"
description="Please contact website technical administrator."
/>
)}
</>
)
}
</div>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#row {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 1em;
align-items: center;
padding: 1em;
border-radius: 1em;
box-shadow: 0 1px 2px 0 var(--color-shadow-2);
backdrop-filter: blur(10px);
background-color: color-mix(in srgb, var(--color-elevation-2) 50%, transparent);
border-top: 1px solid var(--color-elevation-2);
& > #title {
padding-bottom: 0.2em;
}
& > #dots {
width: 100%;
min-width: clamp(1em, 0.5em + 5vw, 5em);
border-bottom: 0.15em dotted var(--color-base-500);
height: 0.6em;
}
# > #range {
}
}
</style>

View File

@ -0,0 +1,47 @@
---
import { Icon } from "astro-icon/components";
import type { EndpointCollectible } from "src/shared/payload/payload-sdk";
import ContentRow from "./ContentRow.astro";
import { getI18n } from "src/i18n/i18n";
interface Props {
contents: EndpointCollectible["contents"];
}
const { contents } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="title">
<Icon name="material-symbols:list-alt-outline" width={24} height={24} />
<p>{t("collectibles.contents")}</p>
</div>
<div id="contents">
{contents.map((content) => <ContentRow content={content} />)}
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#title {
margin-top: 6em;
margin-bottom: 2em;
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
}
}
#contents {
display: grid;
gap: 8px;
}
</style>

View File

@ -0,0 +1,58 @@
---
interface Props {
image: string;
title: string;
subtitle: string;
}
const { image, title, subtitle } = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="tile">
<img src={image} />
<div class="high-contrast-text">
<p class="title">{title}</p>
<p>{subtitle}</p>
</div>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#tile {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
display: grid;
background-color: var(--color-elevation-0);
place-items: center;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
& > div {
text-align: center;
backdrop-filter: blur(5px);
padding: 1em;
border-radius: 1em;
& > .title {
font-size: 130%;
font-weight: 500;
margin-bottom: 0.2em;
}
}
& > img {
position: absolute;
inset: 0;
opacity: 0.5;
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>

View File

@ -0,0 +1,87 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
import {
CollectibleBindingTypes,
CollectiblePageOrders,
type EndpointCollectible,
} from "src/shared/payload/payload-sdk";
interface Props {
pageInfo: NonNullable<EndpointCollectible["pageInfo"]>;
}
const {
pageInfo: { pageCount, bindingType, pageOrder },
} = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container">
<div id="title">
<Icon name="material-symbols:note-stack-outline" width={24} height={24} />
<p>{t("collectibles.bookFormat")}</p>
</div>
<div id="values">
<p>{t("collectibles.bookFormat.pageCount", { count: pageCount })}</p>
{
bindingType && (
<p>
{t(
bindingType === CollectibleBindingTypes.Hardcover
? "collectibles.bookFormat.binding.hardcover"
: "collectibles.bookFormat.binding.paperback"
)}
</p>
)
}
{
pageOrder && (
<p>
{t(
pageOrder === CollectiblePageOrders.LeftToRight
? "collectibles.bookFormat.binding.readingDirection.leftToRight"
: "collectibles.bookFormat.binding.readingDirection.rightToLeft"
)}
</p>
)
}
</div>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5em 1em;
align-items: start;
@media (max-width: 35em) {
grid-template-columns: 1fr;
}
& > #title {
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
}
}
& > #values {
display: flex;
flex-direction: column;
gap: 0.5em;
margin-top: 0.5em;
}
}
</style>

View File

@ -0,0 +1,70 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
import { convert } from "src/utils/currencies";
interface Props {
price: {
amount: number;
currency: string;
};
}
const { price } = Astro.props;
const { formatPrice, t } = await getI18n(Astro.locals.currentLocale);
const preferredCurrency = Astro.locals.currentCurrency;
const convertedPrice: Props["price"] = {
amount: convert(price.currency, preferredCurrency, price.amount),
currency: preferredCurrency,
};
let priceText = price.amount === 0 ? t("collectibles.price.free") : formatPrice(price);
if (price.amount > 0 && price.currency !== convertedPrice.currency) {
priceText += ` (${formatPrice(convertedPrice)})`;
}
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container">
<div id="title">
<Icon name="material-symbols:sell-outline" width={24} height={24} />
<p>{t("collectibles.price")}</p>
</div>
{(<p>{priceText}</p>)}
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5em 1em;
align-items: start;
@media (max-width: 35em) {
grid-template-columns: 1fr;
}
& > #title {
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
}
}
& > p {
margin-top: 0.5em;
}
}
</style>

View File

@ -0,0 +1,53 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
interface Props {
releaseDate: string;
}
const { releaseDate } = Astro.props;
const { formatDate, t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container">
<div id="title">
<Icon name="material-symbols:calendar-month-outline" width={24} height={24} />
<p>{t("collectibles.releaseDate")}</p>
</div>
<p>{formatDate(new Date(releaseDate))}</p>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5em 1em;
align-items: center;
@media (max-width: 35em) {
grid-template-columns: 1fr;
}
& > #title {
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
}
}
& > p {
margin-top: 0.5em;
}
}
</style>

View File

@ -0,0 +1,91 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
interface Props {
size: {
width: number;
height: number;
thickness?: number;
};
}
const { size } = Astro.props;
const { formatInches, formatMillimeters, t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="size">
<div id="title">
<Icon name="material-symbols:measuring-tape-outline" width={24} height={24} />
<p>{t("collectibles.size")}</p>
</div>
<div id="values">
<div>
<p>{t("collectibles.size.width")}</p>
<p>{formatMillimeters(size.width)}</p>
<p>{formatInches(size.width)}</p>
</div>
<div>
<p>{t("collectibles.size.height")}</p>
<p>{formatMillimeters(size.height)}</p>
<p>{formatInches(size.height)}</p>
</div>
{
size.thickness && (
<div>
<p>{t("collectibles.size.thickness")}</p>
<p>{formatMillimeters(size.thickness)}</p>
<p>{formatInches(size.thickness)}</p>
</div>
)
}
</div>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#size {
display: grid;
grid-template-columns: auto 1fr;
gap: 1em 2em;
align-items: start;
@media (max-width: 35em) {
grid-template-columns: 1fr;
}
& > #title {
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
translate: 0px -0.1em;
}
}
& > #values {
display: flex;
gap: 1em 1.5em;
& > div {
display: flex;
flex-direction: column;
gap: 0.6em;
& > p:first-child {
font-size: 120%;
font-weight: 500;
margin-top: 3px;
}
}
}
}
</style>

View File

@ -0,0 +1,48 @@
---
import { Icon } from "astro-icon/components";
import CollectiblePreview from "components/Previews/CollectiblePreview.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointCollectible } from "src/shared/payload/payload-sdk";
interface Props {
subitems: EndpointCollectible["subitems"];
}
const { subitems } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="title">
<Icon name="material-symbols:box-outline" width={24} height={24} />
<p>{t("collectibles.contents")}</p>
</div>
<div id="values">
{subitems.map((subitem) => <CollectiblePreview collectible={subitem} />)}
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#title {
margin-top: 6em;
margin-bottom: 2em;
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
}
}
#values {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: clamp(6px, 2vmin, 16px);
}
</style>

View File

@ -0,0 +1,52 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
interface Props {
weight: number;
}
const { weight } = Astro.props;
const { formatPounds, formatGrams, t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container">
<div id="title">
<Icon name="material-symbols:scale-outline" width={24} height={24} />
<p>{t("collectibles.weight")}</p>
</div>
<p>{formatGrams(weight)}{" "}({formatPounds(weight)})</p>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5em 1em;
align-items: center;
@media (max-width: 35em) {
grid-template-columns: 1fr;
}
& > #title {
display: flex;
place-items: center;
gap: 8px;
& > p {
font-size: 1.5em;
font-weight: 600;
}
}
& > p {
margin-top: 0.5em;
}
}
</style>

View File

@ -6,9 +6,11 @@ import FoldersSection from "./_components/FoldersSection.astro";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
import ErrorMessage from "components/ErrorMessage.astro"; import ErrorMessage from "components/ErrorMessage.astro";
import { getI18n } from "src/i18n/i18n"; import { getI18n } from "src/i18n/i18n";
import CollectiblePreview from "components/Previews/CollectiblePreview.astro";
import PagePreview from "components/Previews/PagePreview.astro";
const { slug } = Astro.params; const { slug } = Astro.params;
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale); const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const folder = await fetchOr404(() => payload.getFolder(slug!)); const folder = await fetchOr404(() => payload.getFolder(slug!));
if (folder instanceof Response) { if (folder instanceof Response) {
@ -50,19 +52,15 @@ const meta = getLocalizedMatch(folder.translations);
) )
} }
<div> <div id="files">
{ {
folder.files.map(({ relationTo, value }) => { folder.files.map(({ relationTo, value }) => {
switch (relationTo) { switch (relationTo) {
case "library-items": case "collectibles":
return <p>Library item not supported yet! {value.slug}</p>; return <CollectiblePreview collectible={value} />;
case "pages": case "pages":
return ( return <PagePreview page={value} />;
<a class="pressable" href={getLocalizedUrl(`/pages/${value.slug}`)}>
{value.slug}
</a>
);
default: default:
return ( return (
@ -85,9 +83,16 @@ const meta = getLocalizedMatch(folder.translations);
display: grid; display: grid;
gap: 4em; gap: 4em;
#sections { & > #sections {
display: grid; display: grid;
gap: 2.5em; gap: 2.5em;
} }
& > #files {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: clamp(6px, 2vmin, 16px);
place-items: start;
}
} }
</style> </style>

View File

@ -14,7 +14,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<AppLayout <AppLayout
title="Accords Library" title="Accords Library"
backgroundIllustration="/img/bg-home2.webp" backgroundIllustration="/img/background-image.webp"
hideFooterLinks hideFooterLinks
hideHomeButton> hideHomeButton>
<div id="title" slot="header-title"> <div id="title" slot="header-title">

View File

@ -14,6 +14,8 @@ if (page instanceof Response) {
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout parentPages={page.parentPages} backgroundIllustration={page.thumbnail?.url}> <AppEmptyLayout
parentPages={page.parentPages}
backgroundIllustration={page.backgroundImage?.url ?? page.thumbnail?.url}>
<Page slug={page.slug} lang={Astro.locals.currentLocale} page={page} /> <Page slug={page.slug} lang={Astro.locals.currentLocale} page={page} />
</AppEmptyLayout> </AppEmptyLayout>

View File

@ -2,6 +2,8 @@
import AppLayout from "components/AppLayout/AppLayout.astro"; import AppLayout from "components/AppLayout/AppLayout.astro";
import { getI18n } from "src/i18n/i18n"; import { getI18n } from "src/i18n/i18n";
import { cache } from "src/utils/cachedPayload"; import { cache } from "src/utils/cachedPayload";
import { formatCurrency } from "src/utils/currencies";
import { formatLocale } from "src/utils/format";
const { currentLocale, currentTheme, currentCurrency } = Astro.locals; const { currentLocale, currentTheme, currentCurrency } = Astro.locals;
const { t } = await getI18n(currentLocale); const { t } = await getI18n(currentLocale);
@ -20,7 +22,7 @@ const { t } = await getI18n(currentLocale);
class:list={{ current: currentLocale === id }} class:list={{ current: currentLocale === id }}
href={`?action-lang=${id}`} href={`?action-lang=${id}`}
data-astro-prefetch="tap"> data-astro-prefetch="tap">
{id} {formatLocale(id)}
</a> </a>
)) ))
} }
@ -50,15 +52,15 @@ const { t } = await getI18n(currentLocale);
</div> </div>
<div class="section"> <div class="section">
<h2>{t("settings.theme.title")}</h2> <h2>{t("settings.currency.title")}</h2>
<p>{t("settings.theme.description")}</p><br /> <p>{t("settings.currency.description")}</p><br />
{ {
cache.currencies.map((id) => ( cache.currencies.map((id) => (
<a <a
class:list={{ current: currentCurrency === id }} class:list={{ current: currentCurrency === id }}
href={`?action-currency=${id}`} href={`?action-currency=${id}`}
data-astro-prefetch="tap"> data-astro-prefetch="tap">
{id} {`${id} (${formatCurrency(id)})`}
</a> </a>
)) ))
} }
@ -72,6 +74,7 @@ const { t } = await getI18n(currentLocale);
.section { .section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: start;
gap: 0.5em; gap: 0.5em;
& > .current { & > .current {

View File

@ -35,41 +35,35 @@ export type RecorderBiographies =
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "CategoryTranslations". * via the `definition` "CategoryTranslations".
*/ */
export type CategoryTranslations = export type CategoryTranslations = {
| { language: string | Language;
language: string | Language; name: string;
name: string; id?: string | null;
id?: string | null; }[];
}[]
| null;
export interface Config { export interface Config {
collections: { collections: {
folders: Folder; folders: Folder;
'folders-thumbnails': FoldersThumbnail; 'folders-thumbnails': FoldersThumbnail;
'library-items': LibraryItem;
pages: Page; pages: Page;
'chronology-items': ChronologyItem; 'chronology-items': ChronologyItem;
'chronology-eras': ChronologyEra; 'chronology-eras': ChronologyEra;
weapons: Weapon; weapons: Weapon;
'weapons-groups': WeaponsGroup; 'weapons-groups': WeaponsGroup;
'weapons-thumbnails': WeaponsThumbnail; 'weapons-thumbnails': WeaponsThumbnail;
'library-items-thumbnails': LibraryItemThumbnail;
'library-items-scans': LibraryItemScans;
'library-items-gallery': LibraryItemGallery;
'recorders-thumbnails': RecordersThumbnail; 'recorders-thumbnails': RecordersThumbnail;
files: File;
notes: Note; notes: Note;
videos: Video; videos: Video;
'videos-channels': VideosChannel; 'videos-channels': VideosChannel;
languages: Language; languages: Language;
currencies: Currency; currencies: Currency;
recorders: Recorder; recorders: Recorder;
keys: Key;
tags: Tag; tags: Tag;
'tags-groups': TagsGroup; 'tags-groups': TagsGroup;
images: Image; images: Image;
wordings: Wording; wordings: Wording;
collectibles: Collectible;
'generic-contents': GenericContent;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
}; };
@ -123,8 +117,8 @@ export interface Folder {
files?: files?:
| ( | (
| { | {
relationTo: 'library-items'; relationTo: 'collectibles';
value: string | LibraryItem; value: string | Collectible;
} }
| { | {
relationTo: 'pages'; relationTo: 'pages';
@ -170,21 +164,41 @@ export interface Language {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "library-items". * via the `definition` "collectibles".
*/ */
export interface LibraryItem { export interface Collectible {
id: string; id: string;
itemType?: ('Textual' | 'Audio' | 'Video' | 'Game' | 'Other') | null;
language: string | Language;
slug: string; slug: string;
thumbnail?: string | LibraryItemThumbnail | null; thumbnail?: string | Image | null;
pretitle?: string | null; nature: 'Physical' | 'Digital';
title: string; languages?: (string | Language)[] | null;
subtitle?: string | null; tags?: (string | Tag)[] | null;
digital: boolean; translations: {
language: string | Language;
pretitle?: string | null;
title: string;
subtitle?: string | null;
description?: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
}[];
backgroundImage?: string | Image | null;
gallery?: gallery?:
| { | {
image?: string | LibraryItemGallery | null; image: string | Image;
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
@ -195,141 +209,136 @@ export interface LibraryItem {
typesetters?: (string | Recorder)[] | null; typesetters?: (string | Recorder)[] | null;
coverEnabled?: boolean | null; coverEnabled?: boolean | null;
cover?: { cover?: {
front?: string | LibraryItemScans | null; front?: string | Image | null;
spine?: string | LibraryItemScans | null; spine?: string | Image | null;
back?: string | LibraryItemScans | null; back?: string | Image | null;
insideFront?: string | LibraryItemScans | null; insideFront?: string | Image | null;
insideBack?: string | LibraryItemScans | null; insideBack?: string | Image | null;
flapFront?: string | LibraryItemScans | null; flapFront?: string | Image | null;
flapBack?: string | LibraryItemScans | null; flapBack?: string | Image | null;
insideFlapFront?: string | LibraryItemScans | null; insideFlapFront?: string | Image | null;
insideFlapBack?: string | LibraryItemScans | null; insideFlapBack?: string | Image | null;
}; };
dustjacketEnabled?: boolean | null; dustjacketEnabled?: boolean | null;
dustjacket?: { dustjacket?: {
front?: string | LibraryItemScans | null; front?: string | Image | null;
spine?: string | LibraryItemScans | null; spine?: string | Image | null;
back?: string | LibraryItemScans | null; back?: string | Image | null;
insideFront?: string | LibraryItemScans | null; insideFront?: string | Image | null;
insideSpine?: string | LibraryItemScans | null; insideSpine?: string | Image | null;
insideBack?: string | LibraryItemScans | null; insideBack?: string | Image | null;
flapFront?: string | LibraryItemScans | null; flapFront?: string | Image | null;
flapBack?: string | LibraryItemScans | null; flapBack?: string | Image | null;
insideFlapFront?: string | LibraryItemScans | null; insideFlapFront?: string | Image | null;
insideFlapBack?: string | LibraryItemScans | null; insideFlapBack?: string | Image | null;
}; };
obiEnabled?: boolean | null; obiEnabled?: boolean | null;
obi?: { obi?: {
front?: string | LibraryItemScans | null; front?: string | Image | null;
spine?: string | LibraryItemScans | null; spine?: string | Image | null;
back?: string | LibraryItemScans | null; back?: string | Image | null;
insideFront?: string | LibraryItemScans | null; insideFront?: string | Image | null;
insideSpine?: string | LibraryItemScans | null; insideSpine?: string | Image | null;
insideBack?: string | LibraryItemScans | null; insideBack?: string | Image | null;
flapFront?: string | LibraryItemScans | null; flapFront?: string | Image | null;
flapBack?: string | LibraryItemScans | null; flapBack?: string | Image | null;
insideFlapFront?: string | LibraryItemScans | null; insideFlapFront?: string | Image | null;
insideFlapBack?: string | LibraryItemScans | null; insideFlapBack?: string | Image | null;
}; };
pages?: pages?:
| { | {
page: number; page: number;
image: string | LibraryItemScans; image: string | Image;
id?: string | null;
}[]
| null;
archiveFile?: (string | null) | File;
};
textual?: {
subtype?: (string | null) | Key;
pageCount?: number | null;
bindingType?: ('Paperback' | 'Hardcover') | null;
pageOrder?: ('LeftToRight' | 'RightToLeft') | null;
};
audio?: {
audioSubtype?: (string | null) | Key;
tracks?:
| {
title: string;
file: string | File;
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
}; };
video?: {
subtype?: (string | null) | Key;
};
game?: {
demo?: boolean | null;
platform?: (string | null) | Key;
audioLanguages?: (string | Language)[] | null;
subtitleLanguages?: (string | Language)[] | null;
interfacesLanguages?: (string | Language)[] | null;
};
releaseDate?: string | null;
categories?: (string | Key)[] | null;
sizeEnabled?: boolean | null;
size?: {
width: number;
height: number;
thickness?: number | null;
};
priceEnabled?: boolean | null;
price?: {
amount: number;
currency: string | Currency;
};
translations?:
| {
language: string | Language;
description: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
id?: string | null;
}[]
| null;
urls?: urls?:
| { | {
url: string; url: string;
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
parentItems?: (string | LibraryItem)[] | null; releaseDate?: string | null;
subitems?: (string | LibraryItem)[] | null; priceEnabled?: boolean | null;
price?: {
amount: number;
currency: string | Currency;
};
sizeEnabled?: boolean | null;
size?: {
width: number;
height: number;
thickness?: number | null;
};
weightEnabled?: boolean | null;
weight?: {
amount: number;
};
pageInfoEnabled?: boolean | null;
pageInfo?: {
pageCount: number;
bindingType?: ('Paperback' | 'Hardcover') | null;
pageOrder?: ('Left to right' | 'Right to left') | null;
};
folders?: (string | Folder)[] | null;
parentItems?: (string | Collectible)[] | null;
subitems?: (string | Collectible)[] | null;
contents?: contents?:
| { | {
content: string | Page; content:
pageStart?: number | null; | {
pageEnd?: number | null; relationTo: 'pages';
timeStart?: number | null; value: string | Page;
timeEnd?: number | null; }
note?: { | {
root: { relationTo: 'generic-contents';
children: { value: string | GenericContent;
type: string; };
version: number; range?:
[k: string]: unknown; | (
}[]; | {
direction: ('ltr' | 'rtl') | null; start: number;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; end: number;
indent: number; id?: string | null;
type: string; blockName?: string | null;
version: number; blockType: 'pageRange';
}; }
[k: string]: unknown; | {
} | null; start: string;
end: string;
id?: string | null;
blockName?: string | null;
blockType: 'timeRange';
}
| {
translations?:
| {
language: string | Language;
note: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
id?: string | null;
}[]
| null;
id?: string | null;
blockName?: string | null;
blockType: 'other';
}
)[]
| null;
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
@ -340,11 +349,10 @@ export interface LibraryItem {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "library-items-thumbnails". * via the `definition` "images".
*/ */
export interface LibraryItemThumbnail { export interface Image {
id: string; id: string;
libraryItem?: (string | LibraryItem)[] | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
url?: string | null; url?: string | null;
@ -370,48 +378,40 @@ export interface LibraryItemThumbnail {
filesize?: number | null; filesize?: number | null;
filename?: string | null; filename?: string | null;
}; };
square?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
}; };
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "library-items-gallery". * via the `definition` "tags".
*/ */
export interface LibraryItemGallery { export interface Tag {
id: string; id: string;
name?: string | null;
slug: string;
translations: {
language: string | Language;
name: string;
id?: string | null;
}[];
group: string | TagsGroup;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags-groups".
*/
export interface TagsGroup {
id: string;
slug: string;
icon?: string | null;
translations: {
language: string | Language;
name: string;
id?: string | null;
}[];
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
url?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
sizes?: {
thumb?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
small?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
@ -468,86 +468,6 @@ export interface RecordersThumbnail {
}; };
}; };
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "library-items-scans".
*/
export interface LibraryItemScans {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
sizes?: {
thumb?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
og?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
medium?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
large?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "files".
*/
export interface File {
id: string;
filename: string;
type: 'LibraryScans' | 'LibrarySoundtracks' | 'ContentVideo' | 'ContentAudio';
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "keys".
*/
export interface Key {
id: string;
name: string;
type:
| 'Contents'
| 'LibraryAudio'
| 'LibraryVideo'
| 'LibraryTextual'
| 'LibraryGroup'
| 'Library'
| 'Weapons'
| 'GamePlatforms'
| 'Categories'
| 'Wordings';
translations?: CategoryTranslations;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "currencies". * via the `definition` "currencies".
@ -561,9 +481,10 @@ export interface Currency {
*/ */
export interface Page { export interface Page {
id: string; id: string;
type: 'Content' | 'Article' | 'Generic';
slug: string; slug: string;
type: 'Content' | 'Post' | 'Generic';
thumbnail?: string | Image | null; thumbnail?: string | Image | null;
backgroundImage?: string | Image | null;
tags?: (string | Tag)[] | null; tags?: (string | Tag)[] | null;
authors?: (string | Recorder)[] | null; authors?: (string | Recorder)[] | null;
translations: { translations: {
@ -608,7 +529,7 @@ export interface Page {
id?: string | null; id?: string | null;
}[]; }[];
folders?: (string | Folder)[] | null; folders?: (string | Folder)[] | null;
collectibles?: (string | LibraryItem)[] | null; collectibles?: (string | Collectible)[] | null;
updatedBy: string | Recorder; updatedBy: string | Recorder;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@ -616,71 +537,16 @@ export interface Page {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "images". * via the `definition` "generic-contents".
*/ */
export interface Image { export interface GenericContent {
id: string; id: string;
updatedAt: string; name: string;
createdAt: string; translations: {
url?: string | null; language: string | Language;
filename?: string | null; name: string;
mimeType?: string | null; id?: string | null;
filesize?: number | null; }[];
width?: number | null;
height?: number | null;
sizes?: {
thumb?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
og?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags".
*/
export interface Tag {
id: string;
name?: string | null;
slug: string;
translations?:
| {
language: string | Language;
name: string;
id?: string | null;
}[]
| null;
group: string | TagsGroup;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags-groups".
*/
export interface TagsGroup {
id: string;
slug: string;
icon?: string | null;
translations?:
| {
language: string | Language;
name: string;
id?: string | null;
}[]
| null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@ -786,10 +652,8 @@ export interface Weapon {
id: string; id: string;
slug: string; slug: string;
thumbnail?: string | WeaponsThumbnail | null; thumbnail?: string | WeaponsThumbnail | null;
type: string | Key;
group?: (string | null) | WeaponsGroup; group?: (string | null) | WeaponsGroup;
appearances: { appearances: {
categories: (string | Key)[];
translations: { translations: {
language: string | Language; language: string | Language;
sourceLanguage: string | Language; sourceLanguage: string | Language;
@ -1004,7 +868,7 @@ export interface VideosChannel {
export interface Wording { export interface Wording {
id: string; id: string;
name: string; name: string;
translations?: CategoryTranslations; translations: CategoryTranslations;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@ -1047,7 +911,7 @@ export interface PayloadMigration {
* via the `definition` "SpacerBlock". * via the `definition` "SpacerBlock".
*/ */
export interface SpacerBlock { export interface SpacerBlock {
size: 'Small' | 'Medium' | 'Large' | 'XLarge'; size: 'Small' | 'Medium' | 'Large' | 'Extra Large';
blockType: 'spacerBlock'; blockType: 'spacerBlock';
} }
/** /**
@ -1139,12 +1003,7 @@ export enum Collections {
ChronologyItems = "chronology-items", ChronologyItems = "chronology-items",
Currencies = "currencies", Currencies = "currencies",
Files = "files", Files = "files",
Keys = "keys",
Languages = "languages", Languages = "languages",
LibraryItems = "library-items",
LibraryItemsThumbnails = "library-items-thumbnails",
LibraryItemsScans = "library-items-scans",
LibraryItemsGallery = "library-items-gallery",
Notes = "notes", Notes = "notes",
Pages = "pages", Pages = "pages",
PagesThumbnails = "pages-thumbnails", PagesThumbnails = "pages-thumbnails",
@ -1160,7 +1019,9 @@ export enum Collections {
Tags = "tags", Tags = "tags",
TagsGroups = "tags-groups", TagsGroups = "tags-groups",
Images = "images", Images = "images",
Wordings = "wordings" Wordings = "wordings",
Collectibles = "collectibles",
GenericContents = "generic-contents",
} }
export enum CollectionGroups { export enum CollectionGroups {
@ -1169,19 +1030,6 @@ export enum CollectionGroups {
Meta = "Meta", Meta = "Meta",
} }
export enum KeysTypes {
Contents = "Contents",
LibraryAudio = "Library / Audio",
LibraryVideo = "Library / Video",
LibraryTextual = "Library / Textual",
LibraryGroup = "Library / Group",
Library = "Library",
Weapons = "Weapons",
GamePlatforms = "Game Platforms",
Categories = "Categories",
Wordings = "Wordings",
}
export enum LanguageCodes { export enum LanguageCodes {
en = "English", en = "English",
fr = "French", fr = "French",
@ -1191,31 +1039,27 @@ export enum LanguageCodes {
"zh" = "Chinese", "zh" = "Chinese",
} }
export enum FileTypes { export enum CollectibleBindingTypes {
LibraryScans = "Library / Scans",
LibrarySoundtracks = "Library / Soundtracks",
ContentVideo = "Content / Video",
ContentAudio = "Content / Audio",
}
export enum LibraryItemsTypes {
Textual = "Textual",
Audio = "Audio",
Video = "Video",
Game = "Game",
Other = "Other",
}
export enum LibraryItemsTextualBindingTypes {
Paperback = "Paperback", Paperback = "Paperback",
Hardcover = "Hardcover", Hardcover = "Hardcover",
} }
export enum LibraryItemsTextualPageOrders { export enum CollectiblePageOrders {
LeftToRight = "Left to right", LeftToRight = "Left to right",
RightToLeft = "Right to left", RightToLeft = "Right to left",
} }
export enum CollectibleNature {
Physical = "Physical",
Digital = "Digital",
}
export enum CollectibleContentType {
None = "None",
Indexes = "Index-based",
Pages = "Page-based",
}
export enum RecordersRoles { export enum RecordersRoles {
Admin = "Admin", Admin = "Admin",
Recorder = "Recorder", Recorder = "Recorder",
@ -1235,7 +1079,7 @@ export enum VideoSources {
export enum PageType { export enum PageType {
Content = "Content", Content = "Content",
Article = "Article", Post = "Post",
Generic = "Generic", Generic = "Generic",
} }
@ -1583,12 +1427,12 @@ export type EndpointFolder = EndpointFolderPreview & {
}; };
files: ( files: (
| { | {
relationTo: "library-items"; relationTo: "collectibles";
value: LibraryItem; value: EndpointCollectiblePreview;
} }
| { | {
relationTo: "pages"; relationTo: "pages";
value: Page; value: EndpointPagePreview;
} }
)[]; )[];
}; };
@ -1616,17 +1460,6 @@ export type EndpointRecorder = {
}[]; }[];
}; };
export type EndpointKey = {
id: string;
name: string;
type: Key["type"];
translations: {
language: string;
name: string;
short: string;
}[];
};
export type EndpointWording = { export type EndpointWording = {
name: string; name: string;
translations: { translations: {
@ -1654,7 +1487,7 @@ export type EndpointTagsGroup = {
tags: EndpointTag[]; tags: EndpointTag[];
}; };
export type EndpointPage = { export type EndpointPagePreview = {
slug: string; slug: string;
type: PageType; type: PageType;
thumbnail?: PayloadImage; thumbnail?: PayloadImage;
@ -1662,18 +1495,24 @@ export type EndpointPage = {
tagGroups: TagGroup[]; tagGroups: TagGroup[];
translations: { translations: {
language: string; language: string;
sourceLanguage: string;
pretitle?: string; pretitle?: string;
title: string; title: string;
subtitle?: string; subtitle?: string;
}[];
status: "draft" | "published";
};
export type EndpointPage = EndpointPagePreview & {
backgroundImage?: PayloadImage;
translations: (EndpointPagePreview["translations"][number] & {
sourceLanguage: string;
summary?: RichTextContent; summary?: RichTextContent;
content: RichTextContent; content: RichTextContent;
transcribers: string[]; transcribers: string[];
translators: string[]; translators: string[];
proofreaders: string[]; proofreaders: string[];
toc: TableOfContentEntry[]; toc: TableOfContentEntry[];
}[]; })[];
status: "draft" | "published";
parentPages: ParentPage[]; parentPages: ParentPage[];
}; };
@ -1684,6 +1523,82 @@ export type ParentPage = {
tag: string; tag: string;
}; };
export type EndpointCollectiblePreview = {
slug: string;
thumbnail?: PayloadImage;
translations: {
language: string;
pretitle?: string;
title: string;
subtitle?: string;
description?: RichTextContent;
}[];
tagGroups: TagGroup[];
status: "draft" | "published";
releaseDate?: string;
languages: string[];
};
export type EndpointCollectible = EndpointCollectiblePreview & {
backgroundImage?: PayloadImage;
nature: CollectibleNature;
gallery: PayloadImage[];
scans: PayloadImage[];
urls: { url: string; label: string }[];
price?: {
amount: number;
currency: string;
};
size?: {
width: number;
height: number;
thickness?: number;
};
weight?: number;
pageInfo?: {
pageCount: number;
bindingType?: CollectibleBindingTypes;
pageOrder?: CollectiblePageOrders;
};
subitems: EndpointCollectiblePreview[];
contents: {
content:
| {
relationTo: "pages";
value: EndpointPagePreview;
}
| {
relationTo: "generic-contents";
value: {
translations: {
language: string;
name: string;
}[];
};
};
range?:
| {
type: "pageRange";
start: number;
end: number;
}
| {
type: "timeRange";
start: string;
end: string;
}
| {
type: "other";
translations: {
language: string;
note: RichTextContent;
}[];
};
}[];
parentPages: ParentPage[];
};
export type TagGroup = { slug: string; icon: string; values: string[] }; export type TagGroup = { slug: string; icon: string; values: string[] };
export type TableOfContentEntry = { export type TableOfContentEntry = {
@ -1723,4 +1638,6 @@ export const payload = {
await (await request(payloadApiUrl(Collections.TagsGroups, `all`))).json(), await (await request(payloadApiUrl(Collections.TagsGroups, `all`))).json(),
getPage: async (slug: string): Promise<EndpointPage> => getPage: async (slug: string): Promise<EndpointPage> =>
await (await request(payloadApiUrl(Collections.Pages, `slug/${slug}`))).json(), await (await request(payloadApiUrl(Collections.Pages, `slug/${slug}`))).json(),
getCollectible: async (slug: string): Promise<EndpointCollectible> =>
await (await request(payloadApiUrl(Collections.Collectibles, `slug/${slug}`))).json(),
}; };

View File

@ -12,3 +12,23 @@ export const formatRecorder = (recorderId: string): string => {
return result.username; return result.username;
}; };
export const formatInlineTitle = ({
pretitle,
title,
subtitle,
}: {
pretitle?: string | undefined;
title: string;
subtitle?: string | undefined;
}): string => {
let result = "";
if (pretitle) {
result += `${pretitle}: `;
}
result += title;
if (subtitle) {
result += `${subtitle}`;
}
return result;
};