Support for responsive images

This commit is contained in:
DrMint 2024-05-23 13:42:14 +02:00
parent bb86a5238b
commit d25e6ba711
30 changed files with 440 additions and 149 deletions

View File

@ -1,16 +1,18 @@
# Accord's Library v3.0 # Accord's Library v3.0
## Ongoing
- [Analytics] Add analytics
## Short term ## Short term
- Number of audio players seems limited (on Chrome and Firefox) - Number of audio players seems limited (on Chrome and Firefox)
- Automatically generate different sizes of images
- [RichTextContent] Handle relationship - [RichTextContent] Handle relationship
- [RichTextContent] Add autolink block support - [RichTextContent] Add autolink block support
## Mid term ## Mid term
- Save cookies for longer than just the session - Save cookies for longer than just the session
- [Folders] Support for nameless section
- [Timeline] Error if collectible not published? - [Timeline] Error if collectible not published?
- [Timeline] display source language - [Timeline] display source language
- [Timeline] Add details button in footer with credits + last updated / created - [Timeline] Add details button in footer with credits + last updated / created
@ -18,10 +20,10 @@
- [Videos] Display platform info + channel page - [Videos] Display platform info + channel page
- [JSLess] Provide JS-less alternative for timeline card footers - [JSLess] Provide JS-less alternative for timeline card footers
- [JSLess] Provide JS-less alternative for parent pages - [JSLess] Provide JS-less alternative for parent pages
- [Analytics] Add analytics
## Long term ## Long term
- [Folders] Support for nameless section
- [Scripts] Can't run the scripts using node (ts-node?) - [Scripts] Can't run the scripts using node (ts-node?)
- [Scans] Adapt size of obi based on cover/dustjacket - [Scans] Adapt size of obi based on cover/dustjacket
- [Scans] Order of cover/dustjacket/obi should be based on the book's page order. - [Scans] Order of cover/dustjacket/obi should be based on the book's page order.

8
package-lock.json generated
View File

@ -13,7 +13,7 @@
"@fontsource-variable/murecho": "^5.0.19", "@fontsource-variable/murecho": "^5.0.19",
"@fontsource-variable/vollkorn": "^5.0.21", "@fontsource-variable/vollkorn": "^5.0.21",
"accept-language": "^3.0.18", "accept-language": "^3.0.18",
"astro": "4.8.7", "astro": "4.9.0",
"astro-icon": "^1.1.0", "astro-icon": "^1.1.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
@ -3166,9 +3166,9 @@
} }
}, },
"node_modules/astro": { "node_modules/astro": {
"version": "4.8.7", "version": "4.9.0",
"resolved": "https://registry.npmjs.org/astro/-/astro-4.8.7.tgz", "resolved": "https://registry.npmjs.org/astro/-/astro-4.9.0.tgz",
"integrity": "sha512-bTtv6zv94+U5cQdwy89LWTnocEfJ2Tfy6vpYYSUthfj12HtOpk0ykGW7YBe9a69NHqPwivSOvRjXOxGKigL9qA==", "integrity": "sha512-beb3go5Oh5QDjns7YVxG1r40Flt/cuXB+YFTnVRqbh2NuMpYRoXZqIT+ZNaorleEfrLjLFXfsn1AAKVP+XZ2KA==",
"dependencies": { "dependencies": {
"@astrojs/compiler": "^2.8.0", "@astrojs/compiler": "^2.8.0",
"@astrojs/internal-helpers": "0.4.0", "@astrojs/internal-helpers": "0.4.0",

View File

@ -12,7 +12,7 @@
"script:download-currencies": "npm run scripts/download-currencies.ts", "script:download-currencies": "npm run scripts/download-currencies.ts",
"script:download-wording-keys": "npm run scripts/download-wording-keys.ts", "script:download-wording-keys": "npm run scripts/download-wording-keys.ts",
"prettier": "prettier --write --list-different --plugin=prettier-plugin-astro .", "prettier": "prettier --write --list-different --plugin=prettier-plugin-astro .",
"precommit": "npm run script:download-wording-keys && npm run script:download-payload-sdk && npm run prettier && npm run astro check" "precommit": "npm run script:download-payload-sdk && npm run script:download-wording-keys && npm run prettier && npm run astro check"
}, },
"engines": { "engines": {
"npm": ">=10.0.0", "npm": ">=10.0.0",
@ -24,7 +24,7 @@
"@fontsource-variable/murecho": "^5.0.19", "@fontsource-variable/murecho": "^5.0.19",
"@fontsource-variable/vollkorn": "^5.0.21", "@fontsource-variable/vollkorn": "^5.0.21",
"accept-language": "^3.0.18", "accept-language": "^3.0.18",
"astro": "4.8.7", "astro": "4.9.0",
"astro-icon": "^1.1.0", "astro-icon": "^1.1.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 KiB

View File

@ -1,14 +1,20 @@
--- ---
import type { PayloadImage } from "src/shared/payload/payload-sdk"; import type {
EndpointImage,
EndpointMediaThumbnail,
EndpointScanImage,
} from "src/shared/payload/payload-sdk";
import { getRandomId } from "src/utils/random"; import { getRandomId } from "src/utils/random";
import { sizesToSrcset } from "src/utils/img";
interface Props { interface Props {
img: Pick<PayloadImage, "url" | "width" | "height">; img: EndpointImage | EndpointMediaThumbnail | EndpointScanImage;
} }
const { const {
img: { url, width, height }, img: { url, width, height, sizes },
} = Astro.props; } = Astro.props;
const uniqueId = getRandomId(); const uniqueId = getRandomId();
const style = ` const style = `
@ -22,8 +28,24 @@ const style = `
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<img id={uniqueId} src={url} class="when-no-print when-js" /> <img
<img id={uniqueId} src={url} class="when-no-print when-no-js" /> id={uniqueId}
src={url}
srcset={sizesToSrcset(sizes)}
sizes="100vw"
width={width}
height={height}
class="when-no-print when-js"
/>
<img
id={uniqueId}
src={url}
srcset={sizesToSrcset(sizes)}
sizes="100vw"
width={width}
height={height}
class="when-no-print when-no-js"
/>
{/* ------------------------------------------- CSS -------------------------------------------- */} {/* ------------------------------------------- CSS -------------------------------------------- */}

View File

@ -5,14 +5,20 @@ import "@fontsource-variable/vollkorn";
import "@fontsource-variable/murecho"; import "@fontsource-variable/murecho";
import { getI18n } from "src/i18n/i18n"; import { getI18n } from "src/i18n/i18n";
import AppLayoutSpinner from "./AppLayoutSpinner.astro"; import AppLayoutSpinner from "./AppLayoutSpinner.astro";
import type { EndpointAudio, EndpointVideo, PayloadImage } from "src/shared/payload/payload-sdk"; import type {
EndpointAudio,
EndpointImage,
EndpointMediaThumbnail,
EndpointVideo,
} from "src/shared/payload/payload-sdk";
import { cache } from "src/utils/payload";
interface Props { interface Props {
openGraph?: openGraph?:
| { | {
title?: string | undefined; title?: string | undefined;
description?: string | undefined; description?: string | undefined;
thumbnail?: PayloadImage | undefined; thumbnail?: EndpointImage | EndpointMediaThumbnail | undefined;
audio?: EndpointAudio | undefined; audio?: EndpointAudio | undefined;
video?: EndpointVideo | undefined; video?: EndpointVideo | undefined;
} }
@ -23,19 +29,10 @@ const { currentLocale } = Astro.locals;
const { t } = await getI18n(currentLocale); const { t } = await getI18n(currentLocale);
const { openGraph = {} } = Astro.props; const { openGraph = {} } = Astro.props;
const { const { description = t("global.meta.description"), audio, video } = openGraph;
description = t("global.meta.description"),
thumbnail = { const thumbnail =
filename: "default_og.jpg", openGraph.thumbnail?.openGraph ?? openGraph.thumbnail ?? cache.config.defaultOpenGraphImage;
filesize: 0,
height: 630,
width: 1200,
mimeType: "image/jpeg",
url: "/img/default_og.jpg",
},
audio,
video,
} = openGraph;
const title = openGraph.title const title = openGraph.title
? `${openGraph.title} ${t("global.siteName")}` ? `${openGraph.title} ${t("global.siteName")}`
@ -76,8 +73,6 @@ const { currentTheme } = Astro.locals;
<meta name="twitter:site" content="@AccordsLibrary" /> <meta name="twitter:site" content="@AccordsLibrary" />
<meta name="twitter:title" content={title} /> <meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} /> <meta name="twitter:description" content={description} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={thumbnail.url} />
<meta property="og:type" content={video ? "video.movie" : audio ? "music.song" : "website"} /> <meta property="og:type" content={video ? "video.movie" : audio ? "music.song" : "website"} />
<meta property="og:locale" content={currentLocale} /> <meta property="og:locale" content={currentLocale} />
@ -86,11 +81,20 @@ const { currentTheme } = Astro.locals;
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
{
thumbnail && (
<>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={thumbnail.url} />
<meta property="og:image" content={thumbnail.url} /> <meta property="og:image" content={thumbnail.url} />
<meta property="og:image:secure_url" content={thumbnail.url} /> <meta property="og:image:secure_url" content={thumbnail.url} />
<meta property="og:image:width" content={thumbnail.width.toString()} /> <meta property="og:image:width" content={thumbnail.width.toString()} />
<meta property="og:image:height" content={thumbnail.height.toString()} /> <meta property="og:image:height" content={thumbnail.height.toString()} />
<meta property="og:image:type" content={thumbnail.mimeType} /> <meta property="og:image:type" content={thumbnail.mimeType} />
</>
)
}
{ {
audio && ( audio && (

View File

@ -2,7 +2,9 @@
import Button from "components/Button.astro"; import Button from "components/Button.astro";
import { import {
type EndpointCredit, type EndpointCredit,
type PayloadImage, type EndpointImage,
type EndpointMediaThumbnail,
type EndpointScanImage,
type RichTextContent, type RichTextContent,
} from "src/shared/payload/payload-sdk"; } from "src/shared/payload/payload-sdk";
import Credits from "./Credits.astro"; import Credits from "./Credits.astro";
@ -11,11 +13,12 @@ import AppLayoutTitle from "./AppLayout/components/AppLayoutTitle.astro";
import type { ComponentProps } from "astro/types"; import type { ComponentProps } from "astro/types";
import AppLayoutDescription from "./AppLayout/components/AppLayoutDescription.astro"; import AppLayoutDescription from "./AppLayout/components/AppLayoutDescription.astro";
import Attributes from "./Attributes.astro"; import Attributes from "./Attributes.astro";
import { sizesToSrcset } from "src/utils/img";
interface Props { interface Props {
previousImageHref?: string | undefined; previousImageHref?: string | undefined;
nextImageHref?: string | undefined; nextImageHref?: string | undefined;
image: PayloadImage; image: EndpointImage | EndpointScanImage | EndpointMediaThumbnail;
pretitle?: string | undefined; pretitle?: string | undefined;
title: string; title: string;
subtitle?: string | undefined; subtitle?: string | undefined;
@ -29,7 +32,7 @@ interface Props {
const { const {
nextImageHref, nextImageHref,
previousImageHref, previousImageHref,
image: { url, width, height }, image: { url, width, height, sizes },
attributes = [], attributes = [],
metaAttributes = [], metaAttributes = [],
credits = [], credits = [],
@ -41,24 +44,45 @@ const {
} = Astro.props; } = Astro.props;
const smallTitle = !subtitle && !pretitle; const smallTitle = !subtitle && !pretitle;
const hasNavigation = previousImageHref || nextImageHref;
--- ---
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container"> <div id="container">
<div id="image-viewer" class:list={{ "with-buttons": previousImageHref || nextImageHref }}> <div id="image-viewer" class:list={{ "with-buttons": hasNavigation }}>
{
hasNavigation && (
<a <a
class:list={{ hidden: !previousImageHref }} class:list={{ hidden: !previousImageHref }}
href={previousImageHref} href={previousImageHref}
data-astro-history="replace"> data-astro-history="replace">
<Button icon="material-symbols:chevron-left" /> <Button icon="material-symbols:chevron-left" />
</a> </a>
)
}
<a href={url} target="_blank"><img src={url} width={width} height={height} /></a> <a href={url} target="_blank">
<img
src={url}
srcset={sizesToSrcset(sizes)}
sizes={`(max-aspect-ratio: ${width / 0.9}/${height / 0.7}) 90vw, ${(width / height) * 70}vh`}
width={width}
height={height}
/>
</a>
<a class:list={{ hidden: !nextImageHref }} href={nextImageHref} data-astro-history="replace"> {
hasNavigation && (
<a
class:list={{ hidden: !nextImageHref }}
href={nextImageHref}
data-astro-history="replace">
<Button icon="material-symbols:chevron-right" /> <Button icon="material-symbols:chevron-right" />
</a> </a>
)
}
</div> </div>
<div <div
@ -128,11 +152,11 @@ const smallTitle = !subtitle && !pretitle;
align-items: center; align-items: center;
gap: 2em; gap: 2em;
} }
}
}
& > h1 { h1 {
max-width: 35em; max-width: 35em;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
}
}
</style> </style>

View File

@ -1,13 +1,18 @@
--- ---
import type { PayloadImage } from "src/shared/payload/payload-sdk"; import type {
EndpointImage,
EndpointMediaThumbnail,
EndpointScanImage,
} from "src/shared/payload/payload-sdk";
import Card from "components/Card.astro"; import Card from "components/Card.astro";
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import type { ComponentProps } from "astro/types"; import type { ComponentProps } from "astro/types";
import { getI18n } from "src/i18n/i18n"; import { getI18n } from "src/i18n/i18n";
import InlineAttributes from "components/InlineAttributes.astro"; import InlineAttributes from "components/InlineAttributes.astro";
import { sizesToSrcset, sizesForGridLayout } from "src/utils/img";
interface Props { interface Props {
thumbnail?: PayloadImage | undefined; thumbnail?: EndpointImage | EndpointMediaThumbnail | EndpointScanImage | undefined;
pretitle?: string | undefined; pretitle?: string | undefined;
title: string; title: string;
subtitle?: string | undefined; subtitle?: string | undefined;
@ -41,7 +46,13 @@ const {
<div id="card"> <div id="card">
{ {
thumbnail && ( thumbnail && (
<img src={thumbnail.url} width={thumbnail.width} height={thumbnail.height} alt="" /> <img
src={thumbnail.url}
srcset={sizesToSrcset(thumbnail.sizes)}
sizes={sizesForGridLayout(250, 1.15)}
width={thumbnail.width}
height={thumbnail.height}
/>
) )
} }

View File

@ -5,6 +5,7 @@ import OpenMediaPageButton from "./OpenMediaPageButton.astro";
import { getI18n } from "src/i18n/i18n"; import { getI18n } from "src/i18n/i18n";
import HeaderTitle from "components/HeaderTitle.astro"; import HeaderTitle from "components/HeaderTitle.astro";
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import { sizesToSrcset } from "src/utils/img";
interface Props { interface Props {
node: RichTextUploadImageNode; node: RichTextUploadImageNode;
@ -13,7 +14,7 @@ interface Props {
const { const {
node: { node: {
value: { id, url, width, height, translations }, value: { id, url, width, height, translations, sizes },
}, },
context, context,
} = Astro.props; } = Astro.props;
@ -36,7 +37,15 @@ const mediaPage = getLocalizedUrl(`/images/${id}`);
</HeaderTitle> </HeaderTitle>
) )
} }
<a href={mediaPage}><img src={url} width={width} height={height} /></a> <a href={mediaPage}>
<img
src={url}
srcset={sizesToSrcset(sizes)}
sizes={`(max-width: 550px) 90vw, 550px`}
width={width}
height={height}
/>
</a>
<OpenMediaPageButton url={mediaPage} /> <OpenMediaPageButton url={mediaPage} />
</div> </div>

View File

@ -1,6 +1,9 @@
--- ---
import type { EndpointImage } from "src/shared/payload/payload-sdk";
import { sizesForGridLayout, sizesToSrcset } from "src/utils/img";
interface Props { interface Props {
img?: { light: string; dark: string } | undefined; img?: { light: EndpointImage; dark: EndpointImage } | undefined;
name: string; name: string;
href: string; href: string;
} }
@ -14,8 +17,26 @@ const { img, name, href } = Astro.props;
{ {
img ? ( img ? (
<> <>
<img src={img.light} class="when-light-theme" alt={name} title={name} /> <img
<img src={img.dark} class="when-dark-theme" alt={name} title={name} /> src={img.light.url}
srcset={sizesToSrcset(img.light.sizes)}
sizes={sizesForGridLayout(250, 1.15)}
width={img.light.width}
height={img.light.height}
class="when-light-theme"
alt={name}
title={name}
/>
<img
src={img.dark.url}
srcset={sizesToSrcset(img.dark.sizes)}
sizes={sizesForGridLayout(250, 1.15)}
width={img.dark.width}
height={img.dark.height}
class="when-dark-theme"
alt={name}
title={name}
/>
</> </>
) : ( ) : (
<div> <div>

View File

@ -9,12 +9,10 @@ const { getLocalizedUrl, getLocalizedMatch } = await getI18n(Astro.locals.curren
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
{ {
cache.config.homeFolders.map(({ slug, translations, darkThumbnail, lightThumbnail }) => ( cache.config.home.folders.map(({ slug, translations, darkThumbnail, lightThumbnail }) => (
<CategoryCard <CategoryCard
img={ img={
darkThumbnail && lightThumbnail darkThumbnail && lightThumbnail ? { dark: darkThumbnail, light: lightThumbnail } : undefined
? { dark: darkThumbnail.url, light: lightThumbnail.url }
: undefined
} }
name={getLocalizedMatch(translations).name} name={getLocalizedMatch(translations).name}
href={getLocalizedUrl(`/folders/${slug}`)} href={getLocalizedUrl(`/folders/${slug}`)}

View File

@ -12,6 +12,7 @@ import AppLayoutDescription from "components/AppLayout/components/AppLayoutDescr
import Attributes from "components/Attributes.astro"; import Attributes from "components/Attributes.astro";
import type { Attribute } from "src/utils/attributes"; import type { Attribute } from "src/utils/attributes";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import { sizesToSrcset } from "src/utils/img";
export const partial = true; export const partial = true;
@ -71,6 +72,8 @@ if (updatedBy) {
<img <img
id="thumbnail" id="thumbnail"
src={thumbnail.url} src={thumbnail.url}
srcset={sizesToSrcset(thumbnail.sizes)}
sizes={`(max-width: 550px) 90vw, 550px`}
width={thumbnail.width} width={thumbnail.width}
height={thumbnail.height} height={thumbnail.height}
/> />

View File

@ -11,8 +11,8 @@ import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params; const id = Astro.params.id!;
const audio = await fetchOr404(() => payload.getAudioByID(id!)); const audio = await fetchOr404(() => payload.getAudioByID(id));
if (audio instanceof Response) { if (audio instanceof Response) {
return audio; return audio;
} }
@ -72,10 +72,10 @@ const metaAttributes = [
<AppLayout <AppLayout
openGraph={{ openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail, thumbnail,
audio, audio,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }),
}}> }}>
<div id="container"> <div id="container">
<AudioPlayer audio={audio} /> <AudioPlayer audio={audio} />

View File

@ -1,18 +1,39 @@
--- ---
import type {
EndpointImage,
EndpointMediaThumbnail,
EndpointScanImage,
} from "src/shared/payload/payload-sdk";
import { sizesToSrcset } from "src/utils/img";
interface Props { interface Props {
image: string; image: EndpointImage | EndpointScanImage | EndpointMediaThumbnail;
title: string; title: string;
subtitle: string; subtitle: string;
href: string; href: string;
} }
const { image, title, subtitle, href } = Astro.props; const {
image: { url, width, height, sizes },
title,
subtitle,
href,
} = Astro.props;
--- ---
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<a href={href}> <a href={href}>
<img src={image} /> <img
src={url}
srcset={sizesToSrcset(sizes)}
sizes=`
(max-width: 400px) 90vw,
(max-width: 850px) 50vw,
200px`
width={width}
height={height}
/>
<div class="high-contrast-text"> <div class="high-contrast-text">
<p class="title">{title}</p> <p class="title">{title}</p>

View File

@ -55,9 +55,9 @@ const metaAttributes = [
<AppLayout <AppLayout
openGraph={{ openGraph={{
thumbnail: image,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }), title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail: image,
}} }}
parentPages={parentPages}> parentPages={parentPages}>
<Lightbox <Lightbox

View File

@ -6,6 +6,7 @@ import { getI18n } from "src/i18n/i18n";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
import { sizesToSrcset } from "src/utils/img";
const slug = Astro.params.slug!; const slug = Astro.params.slug!;
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale); const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
@ -38,9 +39,17 @@ const translation = getLocalizedMatch(translations);
<div> <div>
{ {
images.map((image, index) => ( images.map(({ url, width, height, sizes }, index) => (
<a href={getLocalizedUrl(`/collectibles/${slug}/gallery/${index}`)}> <a href={getLocalizedUrl(`/collectibles/${slug}/gallery/${index}`)}>
<img src={image.url} /> <img
src={url}
srcset={sizesToSrcset(sizes)}
sizes={`
(max-width: ${(width / height) * 320}px) 90vw,
${(width / height) * 320}px`}
width={width}
height={height}
/>
</a> </a>
)) ))
} }
@ -70,7 +79,7 @@ const translation = getLocalizedMatch(translations);
} }
& > img { & > img {
max-height: 20em; max-height: 320px;
max-width: 100%; max-width: 100%;
height: auto; height: auto;
width: auto; width: auto;

View File

@ -18,13 +18,14 @@ import { convert } from "src/utils/currencies";
import Attributes from "components/Attributes.astro"; import Attributes from "components/Attributes.astro";
import type { Attribute } from "src/utils/attributes"; import type { Attribute } from "src/utils/attributes";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import { sizesToSrcset } from "src/utils/img";
const { slug } = Astro.params; const slug = Astro.params.slug!;
const { getLocalizedMatch, getLocalizedUrl, t, formatDate, formatPrice } = await getI18n( const { getLocalizedMatch, getLocalizedUrl, t, formatDate, formatPrice } = await getI18n(
Astro.locals.currentLocale Astro.locals.currentLocale
); );
const collectible = await fetchOr404(() => payload.getCollectible(slug!)); const collectible = await fetchOr404(() => payload.getCollectible(slug));
if (collectible instanceof Response) { if (collectible instanceof Response) {
return collectible; return collectible;
} }
@ -158,6 +159,8 @@ if (price) {
<img <img
id="thumbnail" id="thumbnail"
src={thumbnail.url} src={thumbnail.url}
srcset={sizesToSrcset(thumbnail.sizes)}
sizes={`(max-width: 550px) 90vw, 550px`}
width={thumbnail.width} width={thumbnail.width}
height={thumbnail.height} height={thumbnail.height}
/> />
@ -169,7 +172,7 @@ if (price) {
{ {
gallery && ( gallery && (
<ImageTile <ImageTile
image={gallery.thumbnail.url} image={gallery.thumbnail}
title={t("collectibles.gallery.title")} title={t("collectibles.gallery.title")}
subtitle={t("collectibles.gallery.subtitle", { count: gallery.count })} subtitle={t("collectibles.gallery.subtitle", { count: gallery.count })}
href={getLocalizedUrl(`/collectibles/${slug}/gallery`)} href={getLocalizedUrl(`/collectibles/${slug}/gallery`)}
@ -180,7 +183,7 @@ if (price) {
{ {
scans && ( scans && (
<ImageTile <ImageTile
image={scans.thumbnail.url} image={scans.thumbnail}
title={t("collectibles.scans.title")} title={t("collectibles.scans.title")}
subtitle={t("collectibles.scans.subtitle", { count: scans.count })} subtitle={t("collectibles.scans.subtitle", { count: scans.count })}
href={getLocalizedUrl(`/collectibles/${slug}/scans`)} href={getLocalizedUrl(`/collectibles/${slug}/scans`)}

View File

@ -17,7 +17,7 @@ if (scanPage instanceof Response) {
return scanPage; return scanPage;
} }
const { parentPages, previousIndex, nextIndex, image, translations } = scanPage; const { parentPages, previousIndex, nextIndex, image, translations, thumbnail } = scanPage;
const translation = getLocalizedMatch(translations); const translation = getLocalizedMatch(translations);
--- ---
@ -27,7 +27,7 @@ const translation = getLocalizedMatch(translations);
openGraph={{ openGraph={{
title: `${formatInlineTitle(translation)} (${index})`, title: `${formatInlineTitle(translation)} (${index})`,
description: translation.description && formatRichTextToString(translation.description), description: translation.description && formatRichTextToString(translation.description),
thumbnail: image, thumbnail,
}} }}
parentPages={parentPages}> parentPages={parentPages}>
<Lightbox <Lightbox

View File

@ -1,6 +1,7 @@
--- ---
import { getI18n } from "src/i18n/i18n"; import { getI18n } from "src/i18n/i18n";
import type { EndpointScanImage } from "src/shared/payload/payload-sdk"; import type { EndpointScanImage } from "src/shared/payload/payload-sdk";
import { sizesToSrcset } from "src/utils/img";
interface Props { interface Props {
scan: EndpointScanImage; scan: EndpointScanImage;
@ -8,7 +9,7 @@ interface Props {
} }
const { const {
scan: { url, index, width, height }, scan: { url, index, width, height, sizes },
collectibleSlug, collectibleSlug,
} = Astro.props; } = Astro.props;
@ -18,7 +19,15 @@ const { getLocalizedUrl, formatScanIndexShort } = await getI18n(Astro.locals.cur
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<a href={getLocalizedUrl(`/collectibles/${collectibleSlug}/scans/${index}`)}> <a href={getLocalizedUrl(`/collectibles/${collectibleSlug}/scans/${index}`)}>
<img width={width} height={height} src={url} alt={index} /> <img
src={url}
srcset={sizesToSrcset(sizes)}
sizes={`
(max-width: ${(width / height) * 320}px) 90vw,
${(width / height) * 320}px`}
width={width}
height={height}
/>
<p>{formatScanIndexShort(index)}</p> <p>{formatScanIndexShort(index)}</p>
</a> </a>
@ -39,10 +48,11 @@ const { getLocalizedUrl, formatScanIndexShort } = await getI18n(Astro.locals.cur
} }
& > img { & > img {
max-height: 20em; max-height: 320px;
max-width: 100%; max-width: 100%;
height: auto; height: auto;
width: auto; width: auto;
aspect-ratio: auto 21/29.7;
box-shadow: 0 5px 20px -10px var(--color-shadow); box-shadow: 0 5px 20px -10px var(--color-shadow);
} }
} }

View File

@ -15,9 +15,9 @@ import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro
import AppLayoutDescription from "components/AppLayout/components/AppLayoutDescription.astro"; import AppLayoutDescription from "components/AppLayout/components/AppLayoutDescription.astro";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
const { slug } = Astro.params; const slug = Astro.params.slug!;
const folder = await fetchOr404(() => payload.getFolder(slug!)); const folder = await fetchOr404(() => payload.getFolder(slug));
if (folder instanceof Response) { if (folder instanceof Response) {
return folder; return folder;
} }

View File

@ -6,8 +6,8 @@ import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params; const id = Astro.params.id!;
const image = await fetchOr404(() => payload.getImageByID(id!)); const image = await fetchOr404(() => payload.getImageByID(id));
if (image instanceof Response) { if (image instanceof Response) {
return image; return image;
} }
@ -50,9 +50,9 @@ const metaAttributes = [
<AppLayout <AppLayout
openGraph={{ openGraph={{
thumbnail: image,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }), title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail: image,
}}> }}>
<Lightbox <Lightbox
image={image} image={image}

View File

@ -16,11 +16,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<AppLayout <AppLayout
openGraph={{ title: t("home.title") }} openGraph={{ title: t("home.title") }}
backgroundImage={{ backgroundImage={cache.config.home.backgroundImage}
url: "/img/background-image.webp",
height: 2279,
width: 1920,
}}
hideFooterLinks hideFooterLinks
hideHomeButton> hideHomeButton>
<div id="title"> <div id="title">

View File

@ -6,15 +6,17 @@ import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
const { slug } = Astro.params; const slug = Astro.params.slug!;
const page = await fetchOr404(() => payload.getPage(slug!)); const page = await fetchOr404(() => payload.getPage(slug));
if (page instanceof Response) { if (page instanceof Response) {
return page; return page;
} }
const { parentPages, thumbnail, translations, backgroundImage } = page;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale); const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const meta = getLocalizedMatch(page.translations); const meta = getLocalizedMatch(translations);
--- ---
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
@ -23,9 +25,9 @@ const meta = getLocalizedMatch(page.translations);
openGraph={{ openGraph={{
title: formatInlineTitle(meta), title: formatInlineTitle(meta),
description: meta.summary && formatRichTextToString(meta.summary), description: meta.summary && formatRichTextToString(meta.summary),
thumbnail: page.thumbnail, thumbnail: thumbnail,
}} }}
parentPages={page.parentPages} parentPages={parentPages}
backgroundImage={page.backgroundImage ?? page.thumbnail}> backgroundImage={backgroundImage ?? thumbnail}>
<Page slug={page.slug} lang={Astro.locals.currentLocale} page={page} /> <Page slug={slug} lang={Astro.locals.currentLocale} page={page} />
</AppLayout> </AppLayout>

View File

@ -9,6 +9,7 @@ import { payload } from "src/utils/payload";
import type { Attribute } from "src/utils/attributes"; import type { Attribute } from "src/utils/attributes";
import { formatLocale } from "src/utils/format"; import { formatLocale } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
import { sizesToSrcset } from "src/utils/img";
const id = Astro.params.id!; const id = Astro.params.id!;
const recorder = await fetchOr404(() => payload.getRecorderByID(id)); const recorder = await fetchOr404(() => payload.getRecorderByID(id));
@ -46,7 +47,13 @@ if (languages.length > 0) {
{ {
avatar && ( avatar && (
<Fragment slot="header-aside"> <Fragment slot="header-aside">
<img src={avatar.url} width={avatar.width} height={avatar.height} /> <img
src={avatar.url}
srcset={sizesToSrcset(avatar.sizes)}
sizes={`(max-width: 550px) 90vw, 550px`}
width={avatar.width}
height={avatar.height}
/>
</Fragment> </Fragment>
) )
} }

View File

@ -6,7 +6,6 @@ import AppLayout from "components/AppLayout/AppLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro"; import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Card from "components/Card.astro"; import Card from "components/Card.astro";
import { getI18n } from "src/i18n/i18n"; import { getI18n } from "src/i18n/i18n";
import AppLayoutBackgroundImg from "components/AppLayout/components/AppLayoutBackgroundImg.astro";
import { cache } from "src/utils/payload"; import { cache } from "src/utils/payload";
import type { WordingKey } from "src/i18n/wordings-keys"; import type { WordingKey } from "src/i18n/wordings-keys";
import AppLayoutDescription from "components/AppLayout/components/AppLayoutDescription.astro"; import AppLayoutDescription from "components/AppLayout/components/AppLayoutDescription.astro";
@ -18,14 +17,7 @@ const { getLocalizedUrl, t, formatTimelineDate } = await getI18n(Astro.locals.cu
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout> <AppLayout backgroundImage={cache.config.timeline.backgroundImage}>
<AppLayoutBackgroundImg
img={{
url: "/img/timeline-background.webp",
width: 2478,
height: 4110,
}}
/>
<AppLayoutTitle title={t("timeline.title")} /> <AppLayoutTitle title={t("timeline.title")} />
<AppLayoutDescription description={t("timeline.description")} /> <AppLayoutDescription description={t("timeline.description")} />

View File

@ -11,8 +11,8 @@ import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params; const id = Astro.params.id!;
const video = await fetchOr404(() => payload.getVideoByID(id!)); const video = await fetchOr404(() => payload.getVideoByID(id));
if (video instanceof Response) { if (video instanceof Response) {
return video; return video;
} }
@ -71,10 +71,10 @@ const metaAttributes = [
<AppLayout <AppLayout
openGraph={{ openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail, thumbnail,
video, video,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }),
}}> }}>
<div id="container"> <div id="container">
<VideoPlayer video={video} /> <VideoPlayer video={video} />

View File

@ -55,7 +55,6 @@ export interface Page {
slug: string; slug: string;
thumbnail?: string | Image | null; thumbnail?: string | Image | null;
backgroundImage?: string | Image | null; backgroundImage?: string | Image | null;
tags?: (string | Tag)[] | null;
attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null; attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null;
translations: { translations: {
language: string | Language; language: string | Language;
@ -133,7 +132,6 @@ export interface Image {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
tags?: (string | Tag)[] | null;
attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null; attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null;
credits?: Credits; credits?: Credits;
updatedAt: string; updatedAt: string;
@ -161,6 +159,62 @@ export interface Image {
filesize?: number | null; filesize?: number | null;
filename?: string | null; filename?: string | null;
}; };
"200w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"320w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"480w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"800w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"1280w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"1920w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"2560w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
}; };
} }
/** /**
@ -171,22 +225,6 @@ export interface Language {
id: string; id: string;
name: string; name: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags".
*/
export interface Tag {
id: string;
slug: string;
page?: (string | null) | Page;
translations: {
language: string | Language;
name: string;
id?: string | null;
}[];
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TagsBlock". * via the `definition` "TagsBlock".
@ -215,6 +253,22 @@ export interface Attribute {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags".
*/
export interface Tag {
id: string;
slug: string;
page?: (string | null) | Page;
translations: {
language: string | Language;
name: string;
id?: string | null;
}[];
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "NumberBlock". * via the `definition` "NumberBlock".
@ -373,7 +427,6 @@ export interface Collectible {
thumbnail?: string | Image | null; thumbnail?: string | Image | null;
nature: "Physical" | "Digital"; nature: "Physical" | "Digital";
languages?: (string | Language)[] | null; languages?: (string | Language)[] | null;
tags?: (string | Tag)[] | null;
attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null; attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null;
translations: { translations: {
language: string | Language; language: string | Language;
@ -576,7 +629,31 @@ export interface Scan {
filesize?: number | null; filesize?: number | null;
filename?: string | null; filename?: string | null;
}; };
og?: { "200w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"320w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"480w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"800w"?: {
url?: string | null; url?: string | null;
width?: number | null; width?: number | null;
height?: number | null; height?: number | null;
@ -638,7 +715,6 @@ export interface Audio {
} | null; } | null;
id?: string | null; id?: string | null;
}[]; }[];
tags?: (string | Tag)[] | null;
attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null; attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null;
credits?: Credits; credits?: Credits;
updatedAt: string; updatedAt: string;
@ -681,6 +757,62 @@ export interface MediaThumbnail {
filesize?: number | null; filesize?: number | null;
filename?: string | null; filename?: string | null;
}; };
"200w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"320w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"480w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"800w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"1280w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"1920w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
"2560w"?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
}; };
} }
/** /**
@ -714,7 +846,6 @@ export interface Video {
subfile?: string | VideoSubtitle | null; subfile?: string | VideoSubtitle | null;
id?: string | null; id?: string | null;
}[]; }[];
tags?: (string | Tag)[] | null;
attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null; attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null;
credits?: Credits; credits?: Credits;
platformEnabled?: boolean | null; platformEnabled?: boolean | null;
@ -928,6 +1059,9 @@ export interface PayloadMigration {
*/ */
export interface WebsiteConfig { export interface WebsiteConfig {
id: string; id: string;
homeBackgroundImage: string | Image;
timelineBackgroundImage: string | Image;
defaultOpenGraphImage: string | Image;
homeFolders?: homeFolders?:
| { | {
lightThumbnail?: string | Image | null; lightThumbnail?: string | Image | null;
@ -1393,11 +1527,15 @@ export type EndpointFolder = {
}; };
export type EndpointWebsiteConfig = { export type EndpointWebsiteConfig = {
homeFolders: (EndpointFolder & { home: {
backgroundImage?: EndpointImage;
folders: (EndpointFolder & {
lightThumbnail?: EndpointImage; lightThumbnail?: EndpointImage;
darkThumbnail?: EndpointImage; darkThumbnail?: EndpointImage;
})[]; })[];
};
timeline: { timeline: {
backgroundImage?: EndpointImage;
breaks: number[]; breaks: number[];
eventCount: number; eventCount: number;
eras: { eras: {
@ -1406,6 +1544,7 @@ export type EndpointWebsiteConfig = {
name: string; name: string;
}[]; }[];
}; };
defaultOpenGraphImage?: EndpointImage;
}; };
export type EndpointRecorder = { export type EndpointRecorder = {
@ -1516,7 +1655,7 @@ export type EndpointCollectible = {
backgroundImage?: EndpointImage; backgroundImage?: EndpointImage;
nature: CollectibleNature; nature: CollectibleNature;
gallery?: { count: number; thumbnail: EndpointImage }; gallery?: { count: number; thumbnail: EndpointImage };
scans?: { count: number; thumbnail: PayloadImage }; scans?: { count: number; thumbnail: EndpointScanImage };
urls: { url: string; label: string }[]; urls: { url: string; label: string }[];
price?: { price?: {
amount: number; amount: number;
@ -1681,6 +1820,7 @@ export type EndpointCollectibleScanPage = {
export type EndpointScanImage = PayloadImage & { export type EndpointScanImage = PayloadImage & {
index: string; index: string;
sizes: PayloadImage[];
}; };
export type TableOfContentEntry = { export type TableOfContentEntry = {
@ -1748,15 +1888,17 @@ export type EndpointMedia = {
export type EndpointImage = EndpointMedia & { export type EndpointImage = EndpointMedia & {
width: number; width: number;
height: number; height: number;
sizes: PayloadImage[];
openGraph?: PayloadImage;
}; };
export type EndpointAudio = EndpointMedia & { export type EndpointAudio = EndpointMedia & {
thumbnail?: PayloadImage; thumbnail?: EndpointMediaThumbnail;
duration: number; duration: number;
}; };
export type EndpointVideo = EndpointMedia & { export type EndpointVideo = EndpointMedia & {
thumbnail?: PayloadImage; thumbnail?: EndpointMediaThumbnail;
subtitles: { subtitles: {
language: string; language: string;
url: string; url: string;
@ -1776,6 +1918,11 @@ export type EndpointVideo = EndpointMedia & {
duration: number; duration: number;
}; };
export type EndpointMediaThumbnail = PayloadImage & {
sizes: PayloadImage[];
openGraph?: PayloadImage;
};
export type PayloadMedia = { export type PayloadMedia = {
url: string; url: string;
mimeType: string; mimeType: string;

10
src/utils/img.ts Normal file
View File

@ -0,0 +1,10 @@
import type { PayloadImage } from "src/shared/payload/payload-sdk";
export const sizesToSrcset = (sizes: PayloadImage[]): string =>
sizes.map(({ url, width }) => `${encodeURI(url)} ${width}w`).join(", ");
export const sizesForGridLayout = (targetSize: number, marginMultiplier: number) => `
(max-width: ${targetSize * 2 * marginMultiplier}px) ${100 / 1}vw,
(max-width: ${targetSize * 3 * marginMultiplier}px) ${100 / 2}vw,
(max-width: ${targetSize * 4 * marginMultiplier}px) ${100 / 3}vw,
${targetSize}px`;