diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..306ca86 --- /dev/null +++ b/TODO.md @@ -0,0 +1,9 @@ +# Accord's Library v3.0 + +## Short term + +- Translate new wording keys + +## Long term + +- Anonymous comments diff --git a/bun.lockb b/bun.lockb index e94cbf9..e116f58 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 19dabde..13142be 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/public/img/background-image.webp b/public/img/background-image.webp new file mode 100644 index 0000000..04fdd5f Binary files /dev/null and b/public/img/background-image.webp differ diff --git a/public/img/bg-home.webp b/public/img/bg-home.webp deleted file mode 100644 index 7391d19..0000000 Binary files a/public/img/bg-home.webp and /dev/null differ diff --git a/public/img/bg-home2.webp b/public/img/bg-home2.webp deleted file mode 100644 index 105477e..0000000 Binary files a/public/img/bg-home2.webp and /dev/null differ diff --git a/src/components/AppLayout/components/AppLayoutBackgroundImg.astro b/src/components/AppLayout/components/AppLayoutBackgroundImg.astro index ab3655c..eaf5adc 100644 --- a/src/components/AppLayout/components/AppLayoutBackgroundImg.astro +++ b/src/components/AppLayout/components/AppLayoutBackgroundImg.astro @@ -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> diff --git a/src/components/AppLayout/components/Html.astro b/src/components/AppLayout/components/Html.astro index 69dcc56..bd50a08 100644 --- a/src/components/AppLayout/components/Html.astro +++ b/src/components/AppLayout/components/Html.astro @@ -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> diff --git a/src/components/AppLayout/components/Topbar/Topbar.astro b/src/components/AppLayout/components/Topbar/Topbar.astro index d7fc708..893590a 100644 --- a/src/components/AppLayout/components/Topbar/Topbar.astro +++ b/src/components/AppLayout/components/Topbar/Topbar.astro @@ -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> diff --git a/src/components/AppLayout/components/Topbar/components/ParentPageLink.astro b/src/components/AppLayout/components/Topbar/components/ParentPageLink.astro index 78b238d..df1808d 100644 --- a/src/components/AppLayout/components/Topbar/components/ParentPageLink.astro +++ b/src/components/AppLayout/components/Topbar/components/ParentPageLink.astro @@ -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 -------------------------------------------- */} diff --git a/src/components/AppLayout/components/Topbar/components/ParentPagesButton.astro b/src/components/AppLayout/components/Topbar/components/ParentPagesButton.astro index bf6e0df..74fae03 100644 --- a/src/components/AppLayout/components/Topbar/components/ParentPagesButton.astro +++ b/src/components/AppLayout/components/Topbar/components/ParentPagesButton.astro @@ -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> diff --git a/src/components/Blocks/components/SpacerBlock.astro b/src/components/Blocks/components/SpacerBlock.astro index efa5318..4977aa0 100644 --- a/src/components/Blocks/components/SpacerBlock.astro +++ b/src/components/Blocks/components/SpacerBlock.astro @@ -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, }; --- diff --git a/src/components/Metadata.astro b/src/components/Metadata.astro index e62a3f8..344bd95 100644 --- a/src/components/Metadata.astro +++ b/src/components/Metadata.astro @@ -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); } } } diff --git a/src/components/Previews/CollectiblePreview.astro b/src/components/Previews/CollectiblePreview.astro new file mode 100644 index 0000000..35d4022 --- /dev/null +++ b/src/components/Previews/CollectiblePreview.astro @@ -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} </span>} + <span id="title">{title} </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> diff --git a/src/components/Previews/PagePreview.astro b/src/components/Previews/PagePreview.astro new file mode 100644 index 0000000..929a8c2 --- /dev/null +++ b/src/components/Previews/PagePreview.astro @@ -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} </span>} + <span id="title">{title} </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> diff --git a/src/components/TableOfContent/components/TableOfContentItem.astro b/src/components/TableOfContent/components/TableOfContentItem.astro index 52c5502..cd54a13 100644 --- a/src/components/TableOfContent/components/TableOfContentItem.astro +++ b/src/components/TableOfContent/components/TableOfContentItem.astro @@ -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 { diff --git a/src/components/TagGroups.astro b/src/components/TagGroups.astro index 215e0e9..2bcd8a7 100644 --- a/src/components/TagGroups.astro +++ b/src/components/TagGroups.astro @@ -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> diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index bcaf130..a660765 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -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, + }; }; diff --git a/src/i18n/wordings-keys.ts b/src/i18n/wordings-keys.ts index 1fca0e4..38962d6 100644 --- a/src/i18n/wordings-keys.ts +++ b/src/i18n/wordings-keys.ts @@ -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"; diff --git a/src/pages/[locale]/api/pages/partial.astro b/src/pages/[locale]/api/pages/partial.astro index be768e8..425acb5 100644 --- a/src/pages/[locale]/api/pages/partial.astro +++ b/src/pages/[locale]/api/pages/partial.astro @@ -76,9 +76,13 @@ const translation = getLocalizedMatch(page.translations); <Credits translators={translation.translators} proofreaders={translation.proofreaders} /> </div> - <div class="when-not-large meta-container"> - <TableOfContent toc={translation.toc} /> - </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> diff --git a/src/pages/[locale]/collectibles/[slug].astro b/src/pages/[locale]/collectibles/[slug].astro new file mode 100644 index 0000000..b343449 --- /dev/null +++ b/src/pages/[locale]/collectibles/[slug].astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/AvailabilityInfo.astro b/src/pages/[locale]/collectibles/_components/AvailabilityInfo.astro new file mode 100644 index 0000000..7adfd73 --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/AvailabilityInfo.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/ContentsSection/ContentRow.astro b/src/pages/[locale]/collectibles/_components/ContentsSection/ContentRow.astro new file mode 100644 index 0000000..811bf51 --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/ContentsSection/ContentRow.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/ContentsSection/ContentsSection.astro b/src/pages/[locale]/collectibles/_components/ContentsSection/ContentsSection.astro new file mode 100644 index 0000000..4207d92 --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/ContentsSection/ContentsSection.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/ImageTile.astro b/src/pages/[locale]/collectibles/_components/ImageTile.astro new file mode 100644 index 0000000..ff1c7c0 --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/ImageTile.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/PageInfo.astro b/src/pages/[locale]/collectibles/_components/PageInfo.astro new file mode 100644 index 0000000..082196d --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/PageInfo.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/PriceInfo.astro b/src/pages/[locale]/collectibles/_components/PriceInfo.astro new file mode 100644 index 0000000..c9cf82e --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/PriceInfo.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/ReleaseDateInfo.astro b/src/pages/[locale]/collectibles/_components/ReleaseDateInfo.astro new file mode 100644 index 0000000..f2b0fa8 --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/ReleaseDateInfo.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/SizeInfo.astro b/src/pages/[locale]/collectibles/_components/SizeInfo.astro new file mode 100644 index 0000000..5409097 --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/SizeInfo.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/SubitemSection.astro b/src/pages/[locale]/collectibles/_components/SubitemSection.astro new file mode 100644 index 0000000..d58607a --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/SubitemSection.astro @@ -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> diff --git a/src/pages/[locale]/collectibles/_components/WeightInfo.astro b/src/pages/[locale]/collectibles/_components/WeightInfo.astro new file mode 100644 index 0000000..a4a7b70 --- /dev/null +++ b/src/pages/[locale]/collectibles/_components/WeightInfo.astro @@ -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> diff --git a/src/pages/[locale]/folders/[slug].astro b/src/pages/[locale]/folders/[slug].astro index 0a56c86..e114bad 100644 --- a/src/pages/[locale]/folders/[slug].astro +++ b/src/pages/[locale]/folders/[slug].astro @@ -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> diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro index 48b29d9..1059a24 100644 --- a/src/pages/[locale]/index.astro +++ b/src/pages/[locale]/index.astro @@ -14,7 +14,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale); <AppLayout title="Accord’s Library" - backgroundIllustration="/img/bg-home2.webp" + backgroundIllustration="/img/background-image.webp" hideFooterLinks hideHomeButton> <div id="title" slot="header-title"> diff --git a/src/pages/[locale]/pages/[slug].astro b/src/pages/[locale]/pages/[slug].astro index 13e1c94..ee007a0 100644 --- a/src/pages/[locale]/pages/[slug].astro +++ b/src/pages/[locale]/pages/[slug].astro @@ -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> diff --git a/src/pages/[locale]/settings/index.astro b/src/pages/[locale]/settings/index.astro index bad5942..e6029ef 100644 --- a/src/pages/[locale]/settings/index.astro +++ b/src/pages/[locale]/settings/index.astro @@ -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 { diff --git a/src/shared/payload/payload-sdk.ts b/src/shared/payload/payload-sdk.ts index 68cd782..6a571d1 100644 --- a/src/shared/payload/payload-sdk.ts +++ b/src/shared/payload/payload-sdk.ts @@ -35,41 +35,35 @@ export type RecorderBiographies = * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "CategoryTranslations". */ -export type CategoryTranslations = - | { - language: string | Language; - name: string; - id?: string | null; - }[] - | null; +export type CategoryTranslations = { + language: string | Language; + name: string; + id?: string | 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,21 +164,41 @@ 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; - pretitle?: string | null; - title: string; - subtitle?: string | null; - digital: boolean; + 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; + 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?: | { - image?: string | LibraryItemGallery | null; + image: string | Image; id?: string | null; }[] | null; @@ -195,141 +209,136 @@ export interface LibraryItem { 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; + 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 | 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; + 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 | 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; + 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 | 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; + image: string | Image; 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; + 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: string | Page; - pageStart?: number | null; - pageEnd?: number | null; - timeStart?: number | null; - timeEnd?: number | null; - 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; - } | null; + 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; @@ -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?: - | { - 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; + name: string; + translations: { + language: string | Language; + name: string; + id?: string | 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(), }; diff --git a/src/utils/format.ts b/src/utils/format.ts index af81bcf..d22cc40 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -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; +};