Added pretitle and subtitle to media

This commit is contained in:
DrMint 2024-05-11 01:53:59 +02:00
parent 7700146cf5
commit b0057192c4
15 changed files with 356 additions and 93 deletions

11
TODO.md
View File

@ -2,12 +2,14 @@
## Short term
- [Timeline] inline links to pages not working (timeline 2026/06)
- Fix inconsistency with material-icon in Payload (with or without material-icon prefix)
- [Media] display filename alongside the localized title.
- [Media] Have a title, subtitle, pretitle just like everything else
- Background images some times lack gradient at the bottom and fade in before they could load
- Number of audio players seems limited (on Chrome and Firefox)
- Create a tool to upload scans images and apply them to collectible
- Automatically generate different sizes of images
- Handle relationship in RichText Content
- On most pages (collectibles + pages), there is a gap in the breakpoints where no thumbnail is displayed.
- Add duration on audio/video preview cards
- Add proper localization for formatFilesize, formatInches, formatMillimeters, formatPounds, and formatGrams
## Mid term
@ -24,6 +26,7 @@
- When the tags overflow, the tag group name should be align start (see http://localhost:12499/en/pages/magnitude-negative-chapter-1)
- [SDK] create a initPayload() that return a payload sdk (and stop hard wirring to ENV or node-cache)
- [Videos] see why no video on Firefox and no poster on Chrome https://v3.accords-library.com/en/videos/661b672825d380e548dbb8c8
- [Videos] Display platform info + channel page
## Long term

View File

@ -2,7 +2,6 @@
import Button from "components/Button.astro";
import {
type EndpointCredit,
type EndpointTagsGroup,
type PayloadImage,
type RichTextContent,
} from "src/shared/payload/payload-sdk";
@ -10,14 +9,18 @@ import RichText from "./RichText/RichText.astro";
import TagGroups from "./TagGroups.astro";
import Credits from "./Credits.astro";
import DownloadButton from "./DownloadButton.astro";
import AppLayoutTitle from "./AppLayout/components/AppLayoutTitle.astro";
import type { ComponentProps } from "astro/types";
interface Props {
previousImageHref?: string | undefined;
nextImageHref?: string | undefined;
image: PayloadImage;
pretitle?: string | undefined;
title: string;
subtitle?: string | undefined;
description?: RichTextContent | undefined;
tagGroups?: EndpointTagsGroup[] | undefined;
tagGroups?: ComponentProps<typeof TagGroups>["tagGroups"] | undefined;
credits?: EndpointCredit[] | undefined;
filename?: string | undefined;
}
@ -26,12 +29,16 @@ const {
nextImageHref,
previousImageHref,
image: { url, width, height },
tagGroups,
tagGroups = [],
credits,
description,
pretitle,
title,
subtitle,
filename,
} = Astro.props;
const smallTitle = !subtitle && !pretitle;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
@ -58,12 +65,17 @@ const {
complex:
(tagGroups && tagGroups.length > 0) || (credits && credits.length > 0) || description,
}}>
{
smallTitle ? (
<h1>{title}</h1>
) : (
<AppLayoutTitle pretitle={pretitle} title={title} subtitle={subtitle} />
)
}
{description && <RichText content={description} />}
<div>
{tagGroups && tagGroups.length > 0 && <TagGroups {tagGroups} />}
{tagGroups && tagGroups.length > 0 && <TagGroups tagGroups={tagGroups} />}
{credits && credits.length > 0 && <Credits credits={credits} />}
</div>
{filename && <DownloadButton href={url} filename={filename} />}
</div>
</div>

View File

@ -5,9 +5,10 @@ interface Props {
icon: string;
title: string;
values: string[];
withBorder?: boolean | undefined;
}
const { icon, title, values } = Astro.props;
const { icon, title, values, withBorder = true } = Astro.props;
if (values.length === 0) return;
---
@ -19,7 +20,7 @@ if (values.length === 0) return;
<Icon name={icon} width={24} height={24} />
<p>{title}</p>
</div>
<div id="values">
<div id="values" class:list={{ "with-border": withBorder }}>
{values.map((value) => <div>{value}</div>)}
</div>
</div>
@ -54,13 +55,15 @@ if (values.length === 0) return;
flex-wrap: wrap;
gap: 6px;
&.with-border {
& > div {
border: 1px solid var(--color-base-1000);
border-radius: 9999px;
padding-top: 0.15em;
padding-bottom: 0.25em;
padding-top: 0.15em;
padding-inline: 0.6em;
backdrop-filter: blur(10px);
backdrop-filter: blur(10upx);
}
}
}
}

View File

@ -7,32 +7,39 @@ interface Props {
audio: EndpointAudio;
}
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch, getLocalizedUrl, t, formatDuration } = await getI18n(
Astro.locals.currentLocale
);
const {
audio: { id, translations, tagGroups, filename, thumbnail },
audio: { id, translations, tagGroups, filename, thumbnail, duration },
} = Astro.props;
const { title } = translations.length > 0 ? getLocalizedMatch(translations) : { title: filename };
const { pretitle, title, subtitle } =
translations.length > 0
? getLocalizedMatch(translations)
: { pretitle: undefined, title: filename, subtitle: undefined };
// TODO: Add this later
// const attributes = [
// {
// title: "Duration",
// icon: "material-symbols:hourglass-empty",
// values: [duration.toString()],
// },
// ];
const tagsAndAttributes = [
...tagGroups,
{
title: t("global.media.attributes.duration"),
icon: "material-symbols:hourglass-empty",
values: [formatDuration(duration)],
},
];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<GenericPreview
pretitle={pretitle}
title={title}
subtitle={subtitle}
thumbnail={thumbnail}
href={getLocalizedUrl(`/audios/${id}`)}
tagGroups={tagGroups}
tagGroups={tagsAndAttributes}
icon="material-symbols:music-note"
iconHoverLabel={t("global.previewTypes.audio")}
smallTitle
smallTitle={title === filename}
/>

View File

@ -14,17 +14,22 @@ const {
image: { id, translations, tagGroups, filename },
} = Astro.props;
const { title } = translations.length > 0 ? getLocalizedMatch(translations) : { title: filename };
const { pretitle, title, subtitle } =
translations.length > 0
? getLocalizedMatch(translations)
: { pretitle: undefined, title: filename, subtitle: undefined };
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<GenericPreview
pretitle={pretitle}
title={title}
subtitle={subtitle}
thumbnail={thumbnail}
href={getLocalizedUrl(`/images/${id}`)}
tagGroups={tagGroups}
icon="material-symbols:imagesmode"
iconHoverLabel={t("global.previewTypes.image")}
smallTitle
smallTitle={title === filename}
/>

View File

@ -7,23 +7,39 @@ interface Props {
video: EndpointVideo;
}
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch, getLocalizedUrl, t, formatDuration } = await getI18n(
Astro.locals.currentLocale
);
const {
video: { id, translations, tagGroups, filename, thumbnail },
video: { id, translations, tagGroups, filename, thumbnail, duration },
} = Astro.props;
const { title } = translations.length > 0 ? getLocalizedMatch(translations) : { title: filename };
const { pretitle, title, subtitle } =
translations.length > 0
? getLocalizedMatch(translations)
: { pretitle: undefined, title: filename, subtitle: undefined };
const tagsAndAttributes = [
...tagGroups,
{
title: t("global.media.attributes.duration"),
icon: "material-symbols:hourglass-empty",
values: [formatDuration(duration)],
},
];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<GenericPreview
pretitle={pretitle}
title={title}
subtitle={subtitle}
thumbnail={thumbnail}
href={getLocalizedUrl(`/videos/${id}`)}
tagGroups={tagGroups}
tagGroups={tagsAndAttributes}
icon="material-symbols:smart-display"
iconHoverLabel={t("global.previewTypes.video")}
smallTitle
smallTitle={title === filename}
/>

View File

@ -1,22 +0,0 @@
---
import Metadata from "components/Metadata.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointTagsGroup } from "src/shared/payload/payload-sdk";
interface Props {
tagGroup: EndpointTagsGroup;
}
const {
tagGroup: { icon, translations, tags },
} = Astro.props;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<Metadata
icon={icon}
title={getLocalizedMatch(translations).name}
values={tags.map(({ translations }) => getLocalizedMatch(translations).name)}
/>

View File

@ -1,18 +1,39 @@
---
import type { EndpointTagsGroup } from "src/shared/payload/payload-sdk";
import TagGroup from "./TagGroup.astro";
import { getI18n } from "src/i18n/i18n";
import Metadata from "./Metadata.astro";
interface Props {
tagGroups: EndpointTagsGroup[];
tagGroups: (
| EndpointTagsGroup
| { title: string; icon: string; values: string[]; withBorder?: boolean }
)[];
}
const { tagGroups } = Astro.props;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const groups = tagGroups.map((group) => {
if ("title" in group) {
return group;
} else {
return {
title: getLocalizedMatch(group.translations).name,
icon: group.icon,
values: group.tags.map(({ translations }) => getLocalizedMatch(translations).name),
};
}
});
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div>
{tagGroups.map((tag) => <TagGroup tagGroup={tag} />)}
{
groups.map(({ icon, title, values, withBorder }) => (
<Metadata icon={icon} title={title} values={values} withBorder={withBorder} />
))
}
<slot />
</div>

View File

@ -134,6 +134,25 @@ export const getI18n = async (locale: string) => {
options: Intl.DateTimeFormatOptions | undefined = { dateStyle: "medium" }
): string => date.toLocaleDateString(locale, options);
const formatDuration = (durationInSec: number) => {
const hours = Math.floor(durationInSec / 3600);
durationInSec -= hours * 3600;
const minutes = Math.floor(durationInSec / 60);
durationInSec -= minutes * 60;
const seconds = Math.floor(durationInSec);
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
};
const formatFilesize = (sizeInBytes: number): string => {
if (sizeInBytes < 1_000) return `${formatNumber(sizeInBytes, { maximumFractionDigits: 2 })} B`;
sizeInBytes = sizeInBytes / 1000;
if (sizeInBytes < 1_000) return `${formatNumber(sizeInBytes, { maximumFractionDigits: 2 })} KB`;
sizeInBytes = sizeInBytes / 1000;
if (sizeInBytes < 1_000) return `${formatNumber(sizeInBytes, { maximumFractionDigits: 2 })} MB`;
sizeInBytes = sizeInBytes / 1000;
return `${formatNumber(sizeInBytes, { maximumFractionDigits: 2 })} GB`;
};
const formatInches = (sizeInMm: number): string => {
return (
(sizeInMm * 0.039370078740157).toLocaleString(locale, { maximumFractionDigits: 2 }) + " in"
@ -301,6 +320,7 @@ export const getI18n = async (locale: string) => {
getLocalizedUrl,
formatPrice,
formatDate,
formatDuration,
formatInches,
formatPounds,
formatGrams,
@ -309,5 +329,6 @@ export const getI18n = async (locale: string) => {
formatTimelineDate,
formatEndpointSource,
formatScanIndexShort,
formatFilesize,
};
};

View File

@ -139,4 +139,9 @@ export type WordingKey =
| "global.sources.typeLabel.scans"
| "collectibles.scans.dustjacket.description"
| "collectibles.scans.obi.description"
| "global.sources.typeLabel.gallery";
| "global.sources.typeLabel.gallery"
| "global.media.attributes.filename"
| "global.media.attributes.duration"
| "global.media.attributes.filesize"
| "global.media.attributes.createdAt"
| "global.media.attributes.updatedAt";

View File

@ -1,5 +1,6 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import AudioPlayer from "components/AudioPlayer.astro";
import Credits from "components/Credits.astro";
import DownloadButton from "components/DownloadButton.astro";
@ -7,6 +8,7 @@ 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 { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params;
@ -15,23 +17,81 @@ if (audio instanceof Response) {
return audio;
}
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { translations, tagGroups, filename, url, credits } = audio;
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
Astro.locals.currentLocale
);
const {
translations,
tagGroups,
filename,
url,
credits,
filesize,
createdAt,
updatedAt,
thumbnail,
} = audio;
const { title, description } = getLocalizedMatch(translations);
const { pretitle, title, subtitle, description } = getLocalizedMatch(translations);
const smallTitle = !subtitle && !pretitle;
const tagsAndAttributes = [
...tagGroups,
...(filename && title !== filename
? [
{
title: t("global.media.attributes.filename"),
icon: "material-symbols:audio-file-outline",
values: [filename],
withBorder: false,
},
]
: []),
{
title: t("global.media.attributes.filesize"),
icon: "material-symbols:hard-drive-outline",
values: [formatFilesize(filesize)],
withBorder: false,
},
{
title: t("global.media.attributes.createdAt"),
icon: "material-symbols:calendar-add-on-outline",
values: [formatDate(new Date(createdAt))],
withBorder: false,
},
{
title: t("global.media.attributes.updatedAt"),
icon: "material-symbols:edit-calendar",
values: [formatDate(new Date(updatedAt))],
withBorder: false,
},
];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout>
<AppEmptyLayout
openGraph={{
thumbnail,
audio,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }),
}}>
<div id="container">
<AudioPlayer audio={audio} />
<div>
{
smallTitle ? (
<h1>{title}</h1>
) : (
<AppLayoutTitle pretitle={pretitle} title={title} subtitle={subtitle} />
)
}
{description && <RichText content={description} />}
<div>
{tagGroups.length > 0 && <TagGroups {tagGroups} />}
{tagsAndAttributes.length > 0 && <TagGroups tagGroups={tagsAndAttributes} />}
{credits.length > 0 && <Credits credits={credits} />}
</div>
<DownloadButton href={url} filename={filename} />

View File

@ -7,7 +7,9 @@ import { fetchOr404 } from "src/utils/responses";
const slug = Astro.params.slug!;
const index = Astro.params.index!;
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedUrl, getLocalizedMatch, t, formatDate } = await getI18n(
Astro.locals.currentLocale
);
const galleryImage = await fetchOr404(() => payload.getCollectibleGalleryImage(slug, index));
if (galleryImage instanceof Response) {
@ -15,11 +17,38 @@ if (galleryImage instanceof Response) {
}
const { parentPages, previousIndex, nextIndex, image } = galleryImage;
const { filename, translations, createdAt, updatedAt, credits, tagGroups } = image;
const { title, description } =
image.translations.length > 0
? getLocalizedMatch(image.translations)
: { title: image.filename, description: undefined };
const { pretitle, title, subtitle, description } =
translations.length > 0
? getLocalizedMatch(translations)
: { pretitle: undefined, title: filename, subtitle: undefined, description: undefined };
const tagsAndAttributes = [
...tagGroups,
...(filename && title !== filename
? [
{
title: t("global.media.attributes.filename"),
icon: "material-symbols:unknown-document-outline",
values: [filename],
withBorder: false,
},
]
: []),
{
title: t("global.media.attributes.createdAt"),
icon: "material-symbols:calendar-add-on-outline",
values: [formatDate(new Date(createdAt))],
withBorder: false,
},
{
title: t("global.media.attributes.updatedAt"),
icon: "material-symbols:edit-calendar",
values: [formatDate(new Date(updatedAt))],
withBorder: false,
},
];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
@ -27,7 +56,9 @@ const { title, description } =
<AppEmptyLayout parentPages={parentPages}>
<Lightbox
image={image}
pretitle={pretitle}
title={title}
subtitle={subtitle}
previousImageHref={previousIndex
? getLocalizedUrl(`/collectibles/${slug}/gallery/${previousIndex}`)
: undefined}
@ -35,8 +66,8 @@ const { title, description } =
? getLocalizedUrl(`/collectibles/${slug}/gallery/${nextIndex}`)
: undefined}
description={description}
filename={image.filename}
tagGroups={image.tagGroups}
credits={image.credits}
filename={filename}
tagGroups={tagsAndAttributes}
credits={credits}
/>
</AppEmptyLayout>

View File

@ -3,6 +3,7 @@ import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import Lightbox from "components/Lightbox.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/shared/payload/payload-sdk";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params;
@ -11,24 +12,57 @@ if (image instanceof Response) {
return image;
}
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { filename, translations, tagGroups, credits } = image;
const { getLocalizedMatch, formatDate, t } = await getI18n(Astro.locals.currentLocale);
const { filename, translations, tagGroups, credits, createdAt, updatedAt } = image;
const { title, description } =
const { pretitle, title, subtitle, description } =
translations.length > 0
? getLocalizedMatch(translations)
: { title: filename, description: undefined };
: { pretitle: undefined, title: filename, subtitle: undefined, description: undefined };
const tagsAndAttributes = [
...tagGroups,
...(filename && title !== filename
? [
{
title: t("global.media.attributes.filename"),
icon: "material-symbols:unknown-document-outline",
values: [filename],
withBorder: false,
},
]
: []),
{
title: t("global.media.attributes.createdAt"),
icon: "material-symbols:calendar-add-on-outline",
values: [formatDate(new Date(createdAt))],
withBorder: false,
},
{
title: t("global.media.attributes.updatedAt"),
icon: "material-symbols:edit-calendar",
values: [formatDate(new Date(updatedAt))],
withBorder: false,
},
];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout>
<AppEmptyLayout
openGraph={{
thumbnail: image,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }),
}}>
<Lightbox
image={image}
pretitle={pretitle}
title={title}
subtitle={subtitle}
description={description}
filename={filename}
tagGroups={tagGroups}
tagGroups={tagsAndAttributes}
credits={credits}
/>
</AppEmptyLayout>

View File

@ -1,5 +1,6 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Credits from "components/Credits.astro";
import DownloadButton from "components/DownloadButton.astro";
import RichText from "components/RichText/RichText.astro";
@ -7,6 +8,7 @@ import TagGroups from "components/TagGroups.astro";
import VideoPlayer from "components/VideoPlayer.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/shared/payload/payload-sdk";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params;
@ -15,23 +17,80 @@ if (video instanceof Response) {
return video;
}
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { translations, tagGroups, filename, url, credits } = video;
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
Astro.locals.currentLocale
);
const {
translations,
tagGroups,
filename,
url,
credits,
filesize,
updatedAt,
createdAt,
thumbnail,
} = video;
const { title, description } = getLocalizedMatch(translations);
const { pretitle, title, subtitle, description } = getLocalizedMatch(translations);
const smallTitle = !subtitle && !pretitle;
const tagsAndAttributes = [
...tagGroups,
...(filename && title !== filename
? [
{
title: t("global.media.attributes.filename"),
icon: "material-symbols:video-file-outline",
values: [filename],
withBorder: false,
},
]
: []),
{
title: t("global.media.attributes.filesize"),
icon: "material-symbols:hard-drive-outline",
values: [formatFilesize(filesize)],
withBorder: false,
},
{
title: t("global.media.attributes.createdAt"),
icon: "material-symbols:calendar-add-on-outline",
values: [formatDate(new Date(createdAt))],
withBorder: false,
},
{
title: t("global.media.attributes.updatedAt"),
icon: "material-symbols:edit-calendar",
values: [formatDate(new Date(updatedAt))],
withBorder: false,
},
];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout>
<AppEmptyLayout
openGraph={{
thumbnail,
video,
description: description ? formatRichTextToString(description) : undefined,
title: formatInlineTitle({ pretitle, title, subtitle }),
}}>
<div id="container">
<VideoPlayer video={video} />
<div>
{
smallTitle ? (
<h1>{title}</h1>
) : (
<AppLayoutTitle pretitle={pretitle} title={title} subtitle={subtitle} />
)
}
{description && <RichText content={description} />}
<div>
{tagGroups.length > 0 && <TagGroups {tagGroups} />}
{tagsAndAttributes.length > 0 && <TagGroups tagGroups={tagsAndAttributes} />}
{credits.length > 0 && <Credits credits={credits} />}
</div>
<DownloadButton href={url} filename={filename} />

View File

@ -111,7 +111,9 @@ export interface Image {
translations?:
| {
language: string | Language;
pretitle?: string | null;
title: string;
subtitle?: string | null;
description?: {
root: {
type: string;
@ -559,7 +561,9 @@ export interface Audio {
thumbnail?: string | MediaThumbnail | null;
translations: {
language: string | Language;
pretitle?: string | null;
title: string;
subtitle?: string | null;
description?: {
root: {
type: string;
@ -631,7 +635,9 @@ export interface Video {
thumbnail?: string | MediaThumbnail | null;
translations: {
language: string | Language;
pretitle?: string | null;
title: string;
subtitle?: string | null;
description?: {
root: {
type: string;
@ -1711,7 +1717,9 @@ export type EndpointMedia = {
tagGroups: EndpointTagsGroup[];
translations: {
language: string;
pretitle?: string;
title: string;
subtitle?: string;
description?: RichTextContent;
}[];
credits: EndpointCredit[];