diff --git a/TODO.md b/TODO.md index 6007070..7580a19 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/src/components/Lightbox.astro b/src/components/Lightbox.astro index d865e46..6ff11e3 100644 --- a/src/components/Lightbox.astro +++ b/src/components/Lightbox.astro @@ -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["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, }}> -

{title}

+ { + smallTitle ? ( +

{title}

+ ) : ( + + ) + } + {description && } -
- {tagGroups && tagGroups.length > 0 && } - {credits && credits.length > 0 && } -
+ {tagGroups && tagGroups.length > 0 && } + {credits && credits.length > 0 && } {filename && } diff --git a/src/components/Metadata.astro b/src/components/Metadata.astro index 344bd95..ad2af08 100644 --- a/src/components/Metadata.astro +++ b/src/components/Metadata.astro @@ -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;

{title}

-
+
{values.map((value) =>
{value}
)}
@@ -54,13 +55,15 @@ if (values.length === 0) return; flex-wrap: wrap; gap: 6px; - & > div { - border: 1px solid var(--color-base-1000); - border-radius: 9999px; - padding-top: 0.15em; - padding-bottom: 0.25em; - padding-inline: 0.6em; - backdrop-filter: blur(10px); + &.with-border { + & > div { + border: 1px solid var(--color-base-1000); + border-radius: 9999px; + padding-bottom: 0.25em; + padding-top: 0.15em; + padding-inline: 0.6em; + backdrop-filter: blur(10upx); + } } } } diff --git a/src/components/Previews/AudioPreview.astro b/src/components/Previews/AudioPreview.astro index 60b2cc4..2735ef6 100644 --- a/src/components/Previews/AudioPreview.astro +++ b/src/components/Previews/AudioPreview.astro @@ -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 ------------------------------------------- */} diff --git a/src/components/Previews/ImagePreview.astro b/src/components/Previews/ImagePreview.astro index aa17929..566073a 100644 --- a/src/components/Previews/ImagePreview.astro +++ b/src/components/Previews/ImagePreview.astro @@ -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 ------------------------------------------- */} diff --git a/src/components/Previews/VideoPreview.astro b/src/components/Previews/VideoPreview.astro index ba1cc09..9eaf6a9 100644 --- a/src/components/Previews/VideoPreview.astro +++ b/src/components/Previews/VideoPreview.astro @@ -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 ------------------------------------------- */} diff --git a/src/components/TagGroup.astro b/src/components/TagGroup.astro deleted file mode 100644 index c509186..0000000 --- a/src/components/TagGroup.astro +++ /dev/null @@ -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 ------------------------------------------- */} - - getLocalizedMatch(translations).name)} -/> diff --git a/src/components/TagGroups.astro b/src/components/TagGroups.astro index 5da93f6..f66a328 100644 --- a/src/components/TagGroups.astro +++ b/src/components/TagGroups.astro @@ -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 ------------------------------------------- */}
- {tagGroups.map((tag) => )} + { + groups.map(({ icon, title, values, withBorder }) => ( + + )) + }
diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index faf267d..91c3d22 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -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, }; }; diff --git a/src/i18n/wordings-keys.ts b/src/i18n/wordings-keys.ts index 0622e97..2cdd5ac 100644 --- a/src/i18n/wordings-keys.ts +++ b/src/i18n/wordings-keys.ts @@ -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"; diff --git a/src/pages/[locale]/audios/[id].astro b/src/pages/[locale]/audios/[id].astro index 675f58d..ae34f5c 100644 --- a/src/pages/[locale]/audios/[id].astro +++ b/src/pages/[locale]/audios/[id].astro @@ -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 ------------------------------------------- */} - +
-

{title}

+ { + smallTitle ? ( +

{title}

+ ) : ( + + ) + } {description && }
- {tagGroups.length > 0 && } + {tagsAndAttributes.length > 0 && } {credits.length > 0 && }
diff --git a/src/pages/[locale]/collectibles/[slug]/gallery/[index].astro b/src/pages/[locale]/collectibles/[slug]/gallery/[index].astro index 6146047..22ae37d 100644 --- a/src/pages/[locale]/collectibles/[slug]/gallery/[index].astro +++ b/src/pages/[locale]/collectibles/[slug]/gallery/[index].astro @@ -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 } = diff --git a/src/pages/[locale]/images/[id].astro b/src/pages/[locale]/images/[id].astro index 5700e36..86415d6 100644 --- a/src/pages/[locale]/images/[id].astro +++ b/src/pages/[locale]/images/[id].astro @@ -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 ------------------------------------------- */} - + diff --git a/src/pages/[locale]/videos/[id].astro b/src/pages/[locale]/videos/[id].astro index a9a37af..dd986c5 100644 --- a/src/pages/[locale]/videos/[id].astro +++ b/src/pages/[locale]/videos/[id].astro @@ -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 ------------------------------------------- */} - +
-

{title}

+ { + smallTitle ? ( +

{title}

+ ) : ( + + ) + } {description && }
- {tagGroups.length > 0 && } + {tagsAndAttributes.length > 0 && } {credits.length > 0 && }
diff --git a/src/shared/payload/payload-sdk.ts b/src/shared/payload/payload-sdk.ts index 77b7778..020f254 100644 --- a/src/shared/payload/payload-sdk.ts +++ b/src/shared/payload/payload-sdk.ts @@ -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[];