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
## Ongoing
- [Analytics] Add analytics
## Short term
- Number of audio players seems limited (on Chrome and Firefox)
- Automatically generate different sizes of images
- [RichTextContent] Handle relationship
- [RichTextContent] Add autolink block support
## Mid term
- Save cookies for longer than just the session
- [Folders] Support for nameless section
- [Timeline] Error if collectible not published?
- [Timeline] display source language
- [Timeline] Add details button in footer with credits + last updated / created
@ -18,10 +20,10 @@
- [Videos] Display platform info + channel page
- [JSLess] Provide JS-less alternative for timeline card footers
- [JSLess] Provide JS-less alternative for parent pages
- [Analytics] Add analytics
## Long term
- [Folders] Support for nameless section
- [Scripts] Can't run the scripts using node (ts-node?)
- [Scans] Adapt size of obi based on cover/dustjacket
- [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/vollkorn": "^5.0.21",
"accept-language": "^3.0.18",
"astro": "4.8.7",
"astro": "4.9.0",
"astro-icon": "^1.1.0",
"node-cache": "^5.1.2",
"tippy.js": "^6.3.7",
@ -3166,9 +3166,9 @@
}
},
"node_modules/astro": {
"version": "4.8.7",
"resolved": "https://registry.npmjs.org/astro/-/astro-4.8.7.tgz",
"integrity": "sha512-bTtv6zv94+U5cQdwy89LWTnocEfJ2Tfy6vpYYSUthfj12HtOpk0ykGW7YBe9a69NHqPwivSOvRjXOxGKigL9qA==",
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/astro/-/astro-4.9.0.tgz",
"integrity": "sha512-beb3go5Oh5QDjns7YVxG1r40Flt/cuXB+YFTnVRqbh2NuMpYRoXZqIT+ZNaorleEfrLjLFXfsn1AAKVP+XZ2KA==",
"dependencies": {
"@astrojs/compiler": "^2.8.0",
"@astrojs/internal-helpers": "0.4.0",

View File

@ -12,7 +12,7 @@
"script:download-currencies": "npm run scripts/download-currencies.ts",
"script:download-wording-keys": "npm run scripts/download-wording-keys.ts",
"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": {
"npm": ">=10.0.0",
@ -24,7 +24,7 @@
"@fontsource-variable/murecho": "^5.0.19",
"@fontsource-variable/vollkorn": "^5.0.21",
"accept-language": "^3.0.18",
"astro": "4.8.7",
"astro": "4.9.0",
"astro-icon": "^1.1.0",
"node-cache": "^5.1.2",
"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 { sizesToSrcset } from "src/utils/img";
interface Props {
img: Pick<PayloadImage, "url" | "width" | "height">;
img: EndpointImage | EndpointMediaThumbnail | EndpointScanImage;
}
const {
img: { url, width, height },
img: { url, width, height, sizes },
} = Astro.props;
const uniqueId = getRandomId();
const style = `
@ -22,8 +28,24 @@ const style = `
{/* ------------------------------------------- HTML ------------------------------------------- */}
<img id={uniqueId} src={url} class="when-no-print when-js" />
<img id={uniqueId} src={url} class="when-no-print when-no-js" />
<img
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 -------------------------------------------- */}

View File

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

View File

@ -2,7 +2,9 @@
import Button from "components/Button.astro";
import {
type EndpointCredit,
type PayloadImage,
type EndpointImage,
type EndpointMediaThumbnail,
type EndpointScanImage,
type RichTextContent,
} from "src/shared/payload/payload-sdk";
import Credits from "./Credits.astro";
@ -11,11 +13,12 @@ import AppLayoutTitle from "./AppLayout/components/AppLayoutTitle.astro";
import type { ComponentProps } from "astro/types";
import AppLayoutDescription from "./AppLayout/components/AppLayoutDescription.astro";
import Attributes from "./Attributes.astro";
import { sizesToSrcset } from "src/utils/img";
interface Props {
previousImageHref?: string | undefined;
nextImageHref?: string | undefined;
image: PayloadImage;
image: EndpointImage | EndpointScanImage | EndpointMediaThumbnail;
pretitle?: string | undefined;
title: string;
subtitle?: string | undefined;
@ -29,7 +32,7 @@ interface Props {
const {
nextImageHref,
previousImageHref,
image: { url, width, height },
image: { url, width, height, sizes },
attributes = [],
metaAttributes = [],
credits = [],
@ -41,24 +44,45 @@ const {
} = Astro.props;
const smallTitle = !subtitle && !pretitle;
const hasNavigation = previousImageHref || nextImageHref;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container">
<div id="image-viewer" class:list={{ "with-buttons": previousImageHref || nextImageHref }}>
<div id="image-viewer" class:list={{ "with-buttons": hasNavigation }}>
{
hasNavigation && (
<a
class:list={{ hidden: !previousImageHref }}
href={previousImageHref}
data-astro-history="replace">
<Button icon="material-symbols:chevron-left" />
</a>
)
}
<a href={url} target="_blank"><img src={url} width={width} height={height} /></a>
<a 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" />
</a>
)
}
</div>
<div
@ -128,11 +152,11 @@ const smallTitle = !subtitle && !pretitle;
align-items: center;
gap: 2em;
}
}
}
& > h1 {
h1 {
max-width: 35em;
overflow-wrap: anywhere;
}
}
}
</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 { Icon } from "astro-icon/components";
import type { ComponentProps } from "astro/types";
import { getI18n } from "src/i18n/i18n";
import InlineAttributes from "components/InlineAttributes.astro";
import { sizesToSrcset, sizesForGridLayout } from "src/utils/img";
interface Props {
thumbnail?: PayloadImage | undefined;
thumbnail?: EndpointImage | EndpointMediaThumbnail | EndpointScanImage | undefined;
pretitle?: string | undefined;
title: string;
subtitle?: string | undefined;
@ -41,7 +46,13 @@ const {
<div id="card">
{
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 HeaderTitle from "components/HeaderTitle.astro";
import { Icon } from "astro-icon/components";
import { sizesToSrcset } from "src/utils/img";
interface Props {
node: RichTextUploadImageNode;
@ -13,7 +14,7 @@ interface Props {
const {
node: {
value: { id, url, width, height, translations },
value: { id, url, width, height, translations, sizes },
},
context,
} = Astro.props;
@ -36,7 +37,15 @@ const mediaPage = getLocalizedUrl(`/images/${id}`);
</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} />
</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 {
img?: { light: string; dark: string } | undefined;
img?: { light: EndpointImage; dark: EndpointImage } | undefined;
name: string;
href: string;
}
@ -14,8 +17,26 @@ const { img, name, href } = Astro.props;
{
img ? (
<>
<img src={img.light} class="when-light-theme" alt={name} title={name} />
<img src={img.dark} class="when-dark-theme" alt={name} title={name} />
<img
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>

View File

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

View File

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

View File

@ -11,8 +11,8 @@ import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params;
const audio = await fetchOr404(() => payload.getAudioByID(id!));
const id = Astro.params.id!;
const audio = await fetchOr404(() => payload.getAudioByID(id));
if (audio instanceof Response) {
return audio;
}
@ -72,10 +72,10 @@ const metaAttributes = [
<AppLayout
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail,
audio,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }),
}}>
<div id="container">
<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 {
image: string;
image: EndpointImage | EndpointScanImage | EndpointMediaThumbnail;
title: string;
subtitle: string;
href: string;
}
const { image, title, subtitle, href } = Astro.props;
const {
image: { url, width, height, sizes },
title,
subtitle,
href,
} = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<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">
<p class="title">{title}</p>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
---
import { getI18n } from "src/i18n/i18n";
import type { EndpointScanImage } from "src/shared/payload/payload-sdk";
import { sizesToSrcset } from "src/utils/img";
interface Props {
scan: EndpointScanImage;
@ -8,7 +9,7 @@ interface Props {
}
const {
scan: { url, index, width, height },
scan: { url, index, width, height, sizes },
collectibleSlug,
} = Astro.props;
@ -18,7 +19,15 @@ const { getLocalizedUrl, formatScanIndexShort } = await getI18n(Astro.locals.cur
{/* ------------------------------------------- HTML ------------------------------------------- */}
<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>
</a>
@ -39,10 +48,11 @@ const { getLocalizedUrl, formatScanIndexShort } = await getI18n(Astro.locals.cur
}
& > img {
max-height: 20em;
max-height: 320px;
max-width: 100%;
height: auto;
width: auto;
aspect-ratio: auto 21/29.7;
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 { 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) {
return folder;
}

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { payload } from "src/utils/payload";
import type { Attribute } from "src/utils/attributes";
import { formatLocale } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses";
import { sizesToSrcset } from "src/utils/img";
const id = Astro.params.id!;
const recorder = await fetchOr404(() => payload.getRecorderByID(id));
@ -46,7 +47,13 @@ if (languages.length > 0) {
{
avatar && (
<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>
)
}

View File

@ -6,7 +6,6 @@ import AppLayout from "components/AppLayout/AppLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Card from "components/Card.astro";
import { getI18n } from "src/i18n/i18n";
import AppLayoutBackgroundImg from "components/AppLayout/components/AppLayoutBackgroundImg.astro";
import { cache } from "src/utils/payload";
import type { WordingKey } from "src/i18n/wordings-keys";
import AppLayoutDescription from "components/AppLayout/components/AppLayoutDescription.astro";
@ -18,14 +17,7 @@ const { getLocalizedUrl, t, formatTimelineDate } = await getI18n(Astro.locals.cu
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout>
<AppLayoutBackgroundImg
img={{
url: "/img/timeline-background.webp",
width: 2478,
height: 4110,
}}
/>
<AppLayout backgroundImage={cache.config.timeline.backgroundImage}>
<AppLayoutTitle title={t("timeline.title")} />
<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 { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params;
const video = await fetchOr404(() => payload.getVideoByID(id!));
const id = Astro.params.id!;
const video = await fetchOr404(() => payload.getVideoByID(id));
if (video instanceof Response) {
return video;
}
@ -71,10 +71,10 @@ const metaAttributes = [
<AppLayout
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail,
video,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }),
}}>
<div id="container">
<VideoPlayer video={video} />

View File

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