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": {
"@astrojs/check": "^0.5.6",
"@astrojs/node": "^8.2.1",
"@fontsource-variable/murecho": "^5.0.17",
"@fontsource-variable/vollkorn": "^5.0.19",
"@astrojs/node": "^8.2.3",
"@fontsource-variable/murecho": "^5.0.18",
"@fontsource-variable/vollkorn": "^5.0.20",
"accept-language": "^3.0.18",
"astro": "4.4.6",
"astro": "4.4.15",
"astro-icon": "^1.1.0",
"node-cache": "^5.1.2",
"tippy.js": "^6.3.7",
"ua-parser-js": "^1.0.37"
},
"devDependencies": {
"@iconify-json/material-symbols": "^1.1.73",
"@iconify-json/material-symbols": "^1.1.74",
"@types/ua-parser-js": "^0.7.39",
"astro-meta-tags": "^0.2.1",
"autoprefixer": "^10.4.17",
"bun-types": "^1.0.29",
"autoprefixer": "^10.4.18",
"bun-types": "^1.0.30",
"npm-check-updates": "^16.14.15",
"postcss-preset-env": "^9.4.0",
"postcss-preset-env": "^9.5.0",
"prettier": "^3.2.5",
"prettier-plugin-astro": "^0.13.0",
"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 uniqueId = getRandomId();
const styleNoScript = `
<style>
#${uniqueId} {
opacity: 1;
transition: unset;
}
</style>`;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<img id={uniqueId} src={src} alt={alt} class="when-no-print" />
<noscript set:html={styleNoScript} />
<img id={uniqueId} src={src} alt={alt} class="when-no-print when-js" />
<img src={src} alt={alt} class="when-no-print when-no-js" />
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
img {
opacity: 0;
transition: 3s opacity;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -1;
bottom: 0;
width: 100%;
height: 100vh;
object-fit: cover;
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>

View File

@ -38,19 +38,12 @@ const { currentTheme } = Astro.locals;
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27231e" />
<link rel="manifest" href="/site.webmanifest" />
<style is:global>
.when-no-js {
display: none;
}
</style>
<noscript>
<style is:global>
.when-js {
display: none !important;
}
.when-no-js {
display: initial !important;
visibility: none !important;
opacity: 0 !important;
}
</style>
</noscript>
@ -224,10 +217,17 @@ const { currentTheme } = Astro.locals;
.high-contrast-text {
text-shadow: 0 0 0.6em var(--color-elevation-0);
}
body {
margin: clamp(12px, 3vmin, 24px) clamp(24px, 4vw, 64px);
}
}
html {
position: relative;
color: var(--color-base-1000);
min-height: 100vb;
display: flex;
@media screen {
background-color: var(--color-base-150);
@ -235,8 +235,7 @@ const { currentTheme } = Astro.locals;
}
body {
margin: clamp(12px, 3vmin, 24px) clamp(24px, 4vw, 64px);
min-height: 100vb;
flex: 1;
box-sizing: border-box;
display: flex;
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 {
text-decoration: none;
flex-shrink: 0;
@ -448,3 +465,13 @@ const { currentTheme } = Astro.locals;
}
}
</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">
<Button
icon="material-symbols:settings-outline"
ariaLabel={t("header.topbar.search.tooltip")}
ariaLabel={t("header.topbar.settings.tooltip")}
/>
</a>
</div>

View File

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

View File

@ -18,6 +18,7 @@ const { t } = await getI18n(Astro.locals.currentLocale);
<Tooltip trigger="click">
<div id="tooltip-content" slot="tooltip-content">
{/* TODO: Translate */}
<p>This content is part of these pages:</p>
{parentPages.map((parentPage) => <ParentPageLink parentPage={parentPage} />)}
</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 {
block: SpacerBlock;
@ -7,11 +7,11 @@ interface Props {
const { block } = Astro.props;
const spaceSizeToRem: Record<SpacerBlock["size"], number> = {
Small: 1,
Medium: 2,
Large: 4,
XLarge: 8,
const spaceSizeToRem: Record<SpacerSizes, number> = {
[SpacerSizes.Small]: 1,
[SpacerSizes.Medium]: 2,
[SpacerSizes.Large]: 4,
[SpacerSizes.XLarge]: 8,
};
---

View File

@ -20,7 +20,7 @@ if (values.length === 0) return;
<p>{title}</p>
</div>
<div id="values">
{values.map((value) => <div class="pill">{value}</div>)}
{values.map((value) => <div>{value}</div>)}
</div>
</div>
@ -54,12 +54,13 @@ if (values.length === 0) return;
flex-wrap: wrap;
gap: 6px;
& > .pill {
& > div {
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);
}
}
}

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 ------------------------------------------- */}
<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 && (
<ol>
@ -28,21 +28,6 @@ const { entry } = Astro.props;
<style>
a {
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 {

View File

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

View File

@ -138,5 +138,48 @@ export const getI18n = async (locale: string) => {
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.icons.tooltip"
| "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} />
</div>
{
translation.toc.length > 0 && (
<div class="when-not-large meta-container">
<TableOfContent toc={translation.toc} />
</div>
)
}
<hr />
<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>
<TableOfContent toc={translation.toc} />
{translation.toc.length > 0 && <TableOfContent toc={translation.toc} />}
</div>
</div>
</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 ErrorMessage from "components/ErrorMessage.astro";
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 { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const folder = await fetchOr404(() => payload.getFolder(slug!));
if (folder instanceof Response) {
@ -50,19 +52,15 @@ const meta = getLocalizedMatch(folder.translations);
)
}
<div>
<div id="files">
{
folder.files.map(({ relationTo, value }) => {
switch (relationTo) {
case "library-items":
return <p>Library item not supported yet! {value.slug}</p>;
case "collectibles":
return <CollectiblePreview collectible={value} />;
case "pages":
return (
<a class="pressable" href={getLocalizedUrl(`/pages/${value.slug}`)}>
{value.slug}
</a>
);
return <PagePreview page={value} />;
default:
return (
@ -85,9 +83,16 @@ const meta = getLocalizedMatch(folder.translations);
display: grid;
gap: 4em;
#sections {
& > #sections {
display: grid;
gap: 2.5em;
}
& > #files {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: clamp(6px, 2vmin, 16px);
place-items: start;
}
}
</style>

View File

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

View File

@ -14,6 +14,8 @@ if (page instanceof Response) {
{/* ------------------------------------------- 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} />
</AppEmptyLayout>

View File

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

View File

@ -35,41 +35,35 @@ export type RecorderBiographies =
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "CategoryTranslations".
*/
export type CategoryTranslations =
| {
export type CategoryTranslations = {
language: string | Language;
name: string;
id?: string | null;
}[]
| null;
}[];
export interface Config {
collections: {
folders: Folder;
'folders-thumbnails': FoldersThumbnail;
'library-items': LibraryItem;
pages: Page;
'chronology-items': ChronologyItem;
'chronology-eras': ChronologyEra;
weapons: Weapon;
'weapons-groups': WeaponsGroup;
'weapons-thumbnails': WeaponsThumbnail;
'library-items-thumbnails': LibraryItemThumbnail;
'library-items-scans': LibraryItemScans;
'library-items-gallery': LibraryItemGallery;
'recorders-thumbnails': RecordersThumbnail;
files: File;
notes: Note;
videos: Video;
'videos-channels': VideosChannel;
languages: Language;
currencies: Currency;
recorders: Recorder;
keys: Key;
tags: Tag;
'tags-groups': TagsGroup;
images: Image;
wordings: Wording;
collectibles: Collectible;
'generic-contents': GenericContent;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
@ -123,8 +117,8 @@ export interface Folder {
files?:
| (
| {
relationTo: 'library-items';
value: string | LibraryItem;
relationTo: 'collectibles';
value: string | Collectible;
}
| {
relationTo: 'pages';
@ -170,152 +164,21 @@ export interface Language {
}
/**
* 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;
itemType?: ('Textual' | 'Audio' | 'Video' | 'Game' | 'Other') | null;
language: string | Language;
slug: string;
thumbnail?: string | LibraryItemThumbnail | null;
thumbnail?: string | Image | null;
nature: 'Physical' | 'Digital';
languages?: (string | Language)[] | null;
tags?: (string | Tag)[] | null;
translations: {
language: string | Language;
pretitle?: string | null;
title: string;
subtitle?: string | null;
digital: boolean;
gallery?:
| {
image?: string | LibraryItemGallery | null;
id?: string | null;
}[]
| null;
scansEnabled?: boolean | null;
scans?: {
scanners: (string | Recorder)[];
cleaners: (string | Recorder)[];
typesetters?: (string | Recorder)[] | null;
coverEnabled?: boolean | null;
cover?: {
front?: string | LibraryItemScans | null;
spine?: string | LibraryItemScans | null;
back?: string | LibraryItemScans | null;
insideFront?: string | LibraryItemScans | null;
insideBack?: string | LibraryItemScans | null;
flapFront?: string | LibraryItemScans | null;
flapBack?: string | LibraryItemScans | null;
insideFlapFront?: string | LibraryItemScans | null;
insideFlapBack?: string | LibraryItemScans | null;
};
dustjacketEnabled?: boolean | null;
dustjacket?: {
front?: string | LibraryItemScans | null;
spine?: string | LibraryItemScans | null;
back?: string | LibraryItemScans | null;
insideFront?: string | LibraryItemScans | null;
insideSpine?: string | LibraryItemScans | null;
insideBack?: string | LibraryItemScans | null;
flapFront?: string | LibraryItemScans | null;
flapBack?: string | LibraryItemScans | null;
insideFlapFront?: string | LibraryItemScans | null;
insideFlapBack?: string | LibraryItemScans | null;
};
obiEnabled?: boolean | null;
obi?: {
front?: string | LibraryItemScans | null;
spine?: string | LibraryItemScans | null;
back?: string | LibraryItemScans | null;
insideFront?: string | LibraryItemScans | null;
insideSpine?: string | LibraryItemScans | null;
insideBack?: string | LibraryItemScans | null;
flapFront?: string | LibraryItemScans | null;
flapBack?: string | LibraryItemScans | null;
insideFlapFront?: string | LibraryItemScans | null;
insideFlapBack?: string | LibraryItemScans | null;
};
pages?:
| {
page: number;
image: string | LibraryItemScans;
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;
}[]
| 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?:
| {
url: string;
id?: string | null;
}[]
| null;
parentItems?: (string | LibraryItem)[] | null;
subitems?: (string | LibraryItem)[] | null;
contents?:
| {
content: string | Page;
pageStart?: number | null;
pageEnd?: number | null;
timeStart?: number | null;
timeEnd?: number | null;
note?: {
description?: {
root: {
children: {
type: string;
@ -331,6 +194,152 @@ export interface LibraryItem {
[k: string]: unknown;
} | null;
id?: string | null;
}[];
backgroundImage?: string | Image | null;
gallery?:
| {
image: string | Image;
id?: string | null;
}[]
| null;
scansEnabled?: boolean | null;
scans?: {
scanners: (string | Recorder)[];
cleaners: (string | Recorder)[];
typesetters?: (string | Recorder)[] | null;
coverEnabled?: boolean | null;
cover?: {
front?: string | Image | null;
spine?: string | Image | null;
back?: string | Image | null;
insideFront?: string | Image | null;
insideBack?: string | Image | null;
flapFront?: string | Image | null;
flapBack?: string | Image | null;
insideFlapFront?: string | Image | null;
insideFlapBack?: string | Image | null;
};
dustjacketEnabled?: boolean | null;
dustjacket?: {
front?: string | Image | null;
spine?: string | Image | null;
back?: string | Image | null;
insideFront?: string | Image | null;
insideSpine?: string | Image | null;
insideBack?: string | Image | null;
flapFront?: string | Image | null;
flapBack?: string | Image | null;
insideFlapFront?: string | Image | null;
insideFlapBack?: string | Image | null;
};
obiEnabled?: boolean | null;
obi?: {
front?: string | Image | null;
spine?: string | Image | null;
back?: string | Image | null;
insideFront?: string | Image | null;
insideSpine?: string | Image | null;
insideBack?: string | Image | null;
flapFront?: string | Image | null;
flapBack?: string | Image | null;
insideFlapFront?: string | Image | null;
insideFlapBack?: string | Image | null;
};
pages?:
| {
page: number;
image: string | Image;
id?: string | null;
}[]
| null;
};
urls?:
| {
url: string;
id?: string | null;
}[]
| null;
releaseDate?: string | 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?:
| {
content:
| {
relationTo: 'pages';
value: string | Page;
}
| {
relationTo: 'generic-contents';
value: string | GenericContent;
};
range?:
| (
| {
start: number;
end: number;
id?: string | null;
blockName?: string | null;
blockType: 'pageRange';
}
| {
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;
}[]
| null;
updatedBy: string | Recorder;
@ -340,11 +349,10 @@ export interface LibraryItem {
}
/**
* 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;
libraryItem?: (string | LibraryItem)[] | null;
updatedAt: string;
createdAt: string;
url?: string | null;
@ -370,48 +378,40 @@ export interface LibraryItemThumbnail {
filesize?: number | 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
* via the `definition` "library-items-gallery".
* via the `definition` "tags".
*/
export interface LibraryItemGallery {
export interface Tag {
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;
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
@ -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
* via the `definition` "currencies".
@ -561,9 +481,10 @@ export interface Currency {
*/
export interface Page {
id: string;
type: 'Content' | 'Article' | 'Generic';
slug: string;
type: 'Content' | 'Post' | 'Generic';
thumbnail?: string | Image | null;
backgroundImage?: string | Image | null;
tags?: (string | Tag)[] | null;
authors?: (string | Recorder)[] | null;
translations: {
@ -608,7 +529,7 @@ export interface Page {
id?: string | null;
}[];
folders?: (string | Folder)[] | null;
collectibles?: (string | LibraryItem)[] | null;
collectibles?: (string | Collectible)[] | null;
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
@ -616,71 +537,16 @@ export interface Page {
}
/**
* 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;
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;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags".
*/
export interface Tag {
id: string;
name?: string | null;
slug: string;
translations?:
| {
name: 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;
createdAt: string;
}
@ -786,10 +652,8 @@ export interface Weapon {
id: string;
slug: string;
thumbnail?: string | WeaponsThumbnail | null;
type: string | Key;
group?: (string | null) | WeaponsGroup;
appearances: {
categories: (string | Key)[];
translations: {
language: string | Language;
sourceLanguage: string | Language;
@ -1004,7 +868,7 @@ export interface VideosChannel {
export interface Wording {
id: string;
name: string;
translations?: CategoryTranslations;
translations: CategoryTranslations;
updatedAt: string;
createdAt: string;
}
@ -1047,7 +911,7 @@ export interface PayloadMigration {
* via the `definition` "SpacerBlock".
*/
export interface SpacerBlock {
size: 'Small' | 'Medium' | 'Large' | 'XLarge';
size: 'Small' | 'Medium' | 'Large' | 'Extra Large';
blockType: 'spacerBlock';
}
/**
@ -1139,12 +1003,7 @@ export enum Collections {
ChronologyItems = "chronology-items",
Currencies = "currencies",
Files = "files",
Keys = "keys",
Languages = "languages",
LibraryItems = "library-items",
LibraryItemsThumbnails = "library-items-thumbnails",
LibraryItemsScans = "library-items-scans",
LibraryItemsGallery = "library-items-gallery",
Notes = "notes",
Pages = "pages",
PagesThumbnails = "pages-thumbnails",
@ -1160,7 +1019,9 @@ export enum Collections {
Tags = "tags",
TagsGroups = "tags-groups",
Images = "images",
Wordings = "wordings"
Wordings = "wordings",
Collectibles = "collectibles",
GenericContents = "generic-contents",
}
export enum CollectionGroups {
@ -1169,19 +1030,6 @@ export enum CollectionGroups {
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 {
en = "English",
fr = "French",
@ -1191,31 +1039,27 @@ export enum LanguageCodes {
"zh" = "Chinese",
}
export enum FileTypes {
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 {
export enum CollectibleBindingTypes {
Paperback = "Paperback",
Hardcover = "Hardcover",
}
export enum LibraryItemsTextualPageOrders {
export enum CollectiblePageOrders {
LeftToRight = "Left to right",
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 {
Admin = "Admin",
Recorder = "Recorder",
@ -1235,7 +1079,7 @@ export enum VideoSources {
export enum PageType {
Content = "Content",
Article = "Article",
Post = "Post",
Generic = "Generic",
}
@ -1583,12 +1427,12 @@ export type EndpointFolder = EndpointFolderPreview & {
};
files: (
| {
relationTo: "library-items";
value: LibraryItem;
relationTo: "collectibles";
value: EndpointCollectiblePreview;
}
| {
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 = {
name: string;
translations: {
@ -1654,7 +1487,7 @@ export type EndpointTagsGroup = {
tags: EndpointTag[];
};
export type EndpointPage = {
export type EndpointPagePreview = {
slug: string;
type: PageType;
thumbnail?: PayloadImage;
@ -1662,18 +1495,24 @@ export type EndpointPage = {
tagGroups: TagGroup[];
translations: {
language: string;
sourceLanguage: string;
pretitle?: string;
title: string;
subtitle?: string;
}[];
status: "draft" | "published";
};
export type EndpointPage = EndpointPagePreview & {
backgroundImage?: PayloadImage;
translations: (EndpointPagePreview["translations"][number] & {
sourceLanguage: string;
summary?: RichTextContent;
content: RichTextContent;
transcribers: string[];
translators: string[];
proofreaders: string[];
toc: TableOfContentEntry[];
}[];
status: "draft" | "published";
})[];
parentPages: ParentPage[];
};
@ -1684,6 +1523,82 @@ export type ParentPage = {
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 TableOfContentEntry = {
@ -1723,4 +1638,6 @@ export const payload = {
await (await request(payloadApiUrl(Collections.TagsGroups, `all`))).json(),
getPage: async (slug: string): Promise<EndpointPage> =>
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;
};
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;
};